diff --git a/.config/example.yml b/.config/example.yml index 2924114fb..4146881b1 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -6,10 +6,11 @@ #───┘ URL └───────────────────────────────────────────────────── # Final accessible URL seen by a user. -url: https://example.tld/ - +# Only the host part will be used. # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # URL SETTINGS AFTER THAT! +url: https://example.tld/ + # ┌───────────────────────┐ #───┘ Port and TLS settings └─────────────────────────────────── @@ -45,6 +46,7 @@ db: pass: example-foundkey-pass # Whether to disable query caching + # Default is to cache, i.e. false. #disableCache: true # Extra connection options @@ -57,7 +59,11 @@ db: redis: host: localhost port: 6379 - #family: dual # can be either a number or string (0/dual, 4/ipv4, 6/ipv6) + # Address family to connect over. + # Can be either a number or string (0/dual, 4/ipv4, 6/ipv6) + # Default is "dual". + #family: dual + # The following properties are optional. #pass: example-pass #prefix: example-prefix #db: 1 @@ -65,6 +71,7 @@ redis: # ┌─────────────────────────────┐ #───┘ Elasticsearch configuration └───────────────────────────── +# Elasticsearch is optional. #elasticsearch: # host: localhost # port: 9200 @@ -75,35 +82,41 @@ redis: # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── -# Whether disable HSTS +# Whether to disable HSTS (not recommended) +# Default is to enable HSTS, i.e. false. #disableHsts: true # Number of worker processes by type. -# The sum must not exceed the number of available cores. +# The sum should not exceed the number of available cores. #clusterLimits: # web: 1 # queue: 1 -# Job concurrency per worker -# deliverJobConcurrency: 128 -# inboxJobConcurrency: 16 +# Jobs each worker will try to work on at a time. +#deliverJobConcurrency: 128 +#inboxJobConcurrency: 16 -# Job rate limiter -# deliverJobPerSec: 128 -# inboxJobPerSec: 16 +# Rate limit for each Worker. +# Use -1 to disable. +# A rate limit for deliver jobs is not recommended as it comes with +# a big performance penalty due to overhead of rate limiting. +#deliverJobPerSec: -1 +#inboxJobPerSec: 16 -# Job attempts -# deliverJobMaxAttempts: 12 -# inboxJobMaxAttempts: 8 +# Number of times each job will be tried. +# 1 means only try once and don't retry. +#deliverJobMaxAttempts: 12 +#inboxJobMaxAttempts: 8 # Syslog option #syslog: # host: localhost # port: 514 -# Proxy for HTTP/HTTPS +# Proxy for HTTP/HTTPS outgoing connections #proxy: http://127.0.0.1:3128 +# Hosts that should not be connected to through the proxy specified above #proxyBypassHosts: [ # 'example.com', # '192.0.2.8' @@ -117,7 +130,8 @@ redis: # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: false) +# Proxy remote files +# Default is to not proxy remote files, i.e. false. #proxyRemoteFiles: true # Storage path for files if stored locally (absolute path) @@ -125,11 +139,15 @@ redis: #internalStoragePath: '/etc/foundkey/files' # Upload or download file size limits (bytes) +# default is 262144000 = 250MiB #maxFileSize: 262144000 # Max note text length (in characters) #maxNoteTextLength: 3000 +# By default, Foundkey will fail when something tries to make it fetch something from private IPs. +# With the following setting you can explicitly allow some private CIDR subnets. +# Default is an empty list, i.e. none allowed. #allowedPrivateNetworks: [ # '127.0.0.1/32' #] diff --git a/.mailmap b/.mailmap index 4ae08d92f..51a2eb9dd 100644 --- a/.mailmap +++ b/.mailmap @@ -1,9 +1,9 @@ Andreas Nedbal Andreas Nedbal Balazs Nadasdi -Chloe Kudryavtsev -Chloe Kudryavtsev -Chloe Kudryavtsev +Chloe Kudryavtsev +Chloe Kudryavtsev +Chloe Kudryavtsev Dr. Gutfuck LLC <40531868+gutfuckllc@users.noreply.github.com> Ehsan Javadynia <31900907+ehsanjavadynia@users.noreply.github.com> Francis Dinh diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c5297e68..24c4bbb41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,108 @@ Unreleased changes should not be listed in this file. Instead, run `git shortlog --format='%h %s' --group=trailer:changelog ..` to see unreleased changes; replace `` with the tag you wish to compare from. If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead. +## 13.0.0-preview4 - 2023-02-05 +This release contains 6 breaking changes, including changes to the configuration file format. + +### Added +- new Foundkey logo +- client: add button to unrenote/remove all own renotes +- client: add mod tracker +- client: add button to delete all files of a user for moderators +- server: implement OAuth 2.0 Authorization Code grant +- server: add config for error images +- server: expire notifications after 3 months +- server: start adding /api/v2 routes +- server: indicate Retry-After when rate limiting +- docs: show rate limit information + +### Changed +- **BREAKING** server: implement separate web workers + The configuration file format has been changed: The `clusterLimit` item has been removed + and `clusterLimits` has been added instead. Check the example configuration file. +- **BREAKING** server: remove wildcard blocking and instead block subdomains (#269) + As an administrator you may need to check the list of blocked instances. +- **BREAKING** server: disable deliver rate limit by default + We found that the deliver rate limit causes a lot of load for no real benefit. Because of this, + it will be disabled by default. The default value of `deliverJobPerSec` is set to + disable this rate limit. +- server: adjust permissions for `/api/admin/accounts/delete` + The admin/accounts/delete endpoint now requries administrator privileges + instead of just moderator privileges. +- server: increase nodeinfo caching +- client: headlines in queue widget are links +- client: add tooltips to visibility icons +- server: improve error messages +- server: change default value for `/api/admin/show-users` origin param +- server: lower rate limit for deletion activities + Deleting things that result in federating a delete activity have a more strict rate limit. + This affects the following endpoints: + - `/api/notes/delete` + - `/api/notes/reactions/delete` + - `/api/notes/unrenote` +- server: improve OpenGraph data + - properly render note attachments as RDFa + - add more metadata about e.g. author + - proper OpenGraph data replaces custom `misskey:` RDFa tags +- activitypub: implement [FEP-e232](https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-e232.md) qoutes +- activitypub: use `quoteUri` instead of `quoteUrl` + +### Fixed +- client: fix layout of app authorization page +- client: unify different error dialogs +- client: set display name limit same as server +- client: dont display instance banner tooltip if software name is unknown +- client: fix 500 error in notifications +- client: fix some tooltips not closing +- client: fix issue of search only working once +- client: check `quoteId` for canPost computation +- client: fix quotes with only a CW +- server: fix thread mutes not applying to renotes +- server: fix ReferenceError: meta is undefined +- server: fix TypeError in registerOrFetchInstanceDoc +- server: fix ratelimit in `/api/i/import-following` +- server: handle redirects in signed get +- server: remove reversi database tables +- server: set file permissions after copy +- server: also use human readable URL in search +- server: fix user deletion race condition +- server: add websocket ping mechanism + This should help keep websocket connections alive even if there are no events for + prolonged time periods. This should also fix issues where the "connection has been lost" + dialog appeared despite the connection being fine. +- activitypub: properly parse incoming hashtags +- activitypub: Do block checks more globally +- activitypub: properly render CW only quotes + +### Removed: +- **BREAKING** server: remove Twitter, Github and Discord integrations +ff31b8b06 server: remove bios and cli +a673647fb server: remove avatarColor and bannerColor properties +- **BREAKING** server: remove `api/admin/delete-account`, + You should use the API endpoint `admin/accounts/delete` instead. + It has the same parameter and the same behaviour. +- **BREAKING** remove galleries + Galleries have been removed because low usage and duplication of other behaviour. + Existing gallery posts will be turned into ordinary notes. + If a user had any gallery posts, a new clip called "Gallery" will be created containing + all of the former gallery posts that are now notes. + This affects the following endpoints: + - `/api/gallery/featured` + - `/api/gallery/popular` + - `/api/gallery/posts` + - `/api/gallery/posts/create` + - `/api/gallery/posts/delete` + - `/api/gallery/posts/like` + - `/api/gallery/posts/show` + - `/api/gallery/posts/unlike` + - `/api/i/gallery/likes` + - `/api/i/gallery/posts` + - `/api/users/gallery/posts` +- server: remove application level websocket ping + This pinging mechanism was unused in `foundkey-js`, and we expect other usage to be low. + You can use the pinging mechanism built into the websocket protocol if you wish. + Note that the Server will now also send pings on its own (see *Fixed* section). + ## 13.0.0-preview3 - 2022-12-02 This release contains 1 urgent security fix necessitated by `misskey-forkbomb`. This release contains 1 breaking change. diff --git a/COPYING b/COPYING index 658556e58..bfa3fc62c 100644 --- a/COPYING +++ b/COPYING @@ -21,3 +21,7 @@ https://github.com/deskjet/chiptune2.js#license libopenmpt (as part of openmpt) by OpenMPT License: BSD 3-Clause https://github.com/OpenMPT/openmpt/blob/master/LICENSE + +The logo file (logo.svg) was created by Blinry +License: [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) +https://blinry.org/ diff --git a/README.md b/README.md index 7c3164472..3832b2071 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +
Foundkey logo, an owl holding a key
+ # FoundKey FoundKey is a free and open source microblogging server compatible with ActivityPub. Forked from Misskey, FoundKey improves on maintainability and behaviour, while also bringing in useful features. @@ -10,4 +12,5 @@ FoundKey's documentation is a work in progress. In the meantime, much of the doc If you're interested in helping out with the project, please read the [contributing guide](./CONTRIBUTING.md). ## Sponsors -FoundKey is not interested in sponsorships. +FoundKey is not interested in finanical sponsorships. +We welcome contributions in the forms of code, testing and bug reporting (see also section *Contributing* above). diff --git a/locales/en-US.yml b/locales/en-US.yml index ec0abd644..03a6943f5 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -679,7 +679,6 @@ editCode: "Edit code" apply: "Apply" receiveAnnouncementFromInstance: "Receive notifications from this instance" emailNotification: "Email notifications" -publish: "Publish" useReactionPickerForContextMenu: "Open reaction picker on right-click" typingUsers: "{users} is/are typing..." jumpToSpecifiedDate: "Jump to specific date" @@ -720,11 +719,7 @@ switch: "Switch" noMaintainerInformationWarning: "Maintainer information is not configured." noBotProtectionWarning: "Bot protection is not configured." configure: "Configure" -postToGallery: "Create new gallery post" -attachmentRequired: "At least 1 attachment is required." -gallery: "Gallery" recentPosts: "Recent posts" -popularPosts: "Popular posts" shareWithNote: "Share with note" emailNotConfiguredWarning: "Email address not set." ratio: "Ratio" @@ -864,11 +859,6 @@ _forgotPassword: \ instance administrator instead." contactAdmin: "This instance does not support using email addresses, please contact\ \ the instance administrator to reset your password instead." -_gallery: - my: "My Gallery" - liked: "Liked Posts" - like: "Like" - unlike: "Remove like" _email: _follow: title: "You've got a new follower" @@ -1110,10 +1100,6 @@ _permissions: "write:user-groups": "Create, modify, delete, transfer, join and leave groups. Invite and ban others from groups. Accept and reject group invitations." "read:channels": "List and read followed and joined channels" "write:channels": "Create, modify, follow and unfollow channels" - "read:gallery": "List and read gallery posts" - "write:gallery": "Create, modify and delete gallery posts" - "read:gallery-likes": "List and read gallery post likes" - "write:gallery-likes": "Like and unlike gallery posts" _auth: shareAccess: "Would you like to authorize \"{name}\" to access this account?" shareAccessAsk: "Are you sure you want to authorize this application to access your\ diff --git a/logo.svg b/logo.svg new file mode 100644 index 000000000..fec1e0be9 Binary files /dev/null and b/logo.svg differ diff --git a/package.json b/package.json index 659ba2897..6c01e1f6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foundkey", - "version": "13.0.0-preview3", + "version": "13.0.0-preview4", "repository": { "type": "git", "url": "https://akkoma.dev/FoundKeyGang/FoundKey.git" diff --git a/packages/backend/assets/api-doc.png b/packages/backend/assets/api-doc.png index 9b07f1f39..bc53c252d 100644 Binary files a/packages/backend/assets/api-doc.png and b/packages/backend/assets/api-doc.png differ diff --git a/packages/backend/assets/apple-touch-icon.png b/packages/backend/assets/apple-touch-icon.png index 947c513bb..bc53c252d 100644 Binary files a/packages/backend/assets/apple-touch-icon.png and b/packages/backend/assets/apple-touch-icon.png differ diff --git a/packages/backend/assets/favicon.ico b/packages/backend/assets/favicon.ico index 9be1ff629..2ca6f87ba 100644 Binary files a/packages/backend/assets/favicon.ico and b/packages/backend/assets/favicon.ico differ diff --git a/packages/backend/assets/favicon.png b/packages/backend/assets/favicon.png index b4eb18a5c..bc53c252d 100644 Binary files a/packages/backend/assets/favicon.png and b/packages/backend/assets/favicon.png differ diff --git a/packages/backend/assets/icons/192.png b/packages/backend/assets/icons/192.png index 606b46d87..f22f23bf4 100644 Binary files a/packages/backend/assets/icons/192.png and b/packages/backend/assets/icons/192.png differ diff --git a/packages/backend/assets/icons/512.png b/packages/backend/assets/icons/512.png index ba5154642..24f65815c 100644 Binary files a/packages/backend/assets/icons/512.png and b/packages/backend/assets/icons/512.png differ diff --git a/packages/backend/assets/mi-white.png b/packages/backend/assets/mi-white.png deleted file mode 100644 index 1e57da6b3..000000000 Binary files a/packages/backend/assets/mi-white.png and /dev/null differ diff --git a/packages/backend/assets/splash.png b/packages/backend/assets/splash.png index 3430e6efe..bc53c252d 100644 Binary files a/packages/backend/assets/splash.png and b/packages/backend/assets/splash.png differ diff --git a/packages/backend/migration/1673892262930-remove-groups.js b/packages/backend/migration/1673892262930-remove-groups.js new file mode 100644 index 000000000..9779dd6e0 --- /dev/null +++ b/packages/backend/migration/1673892262930-remove-groups.js @@ -0,0 +1,65 @@ +import { genId } from '../built/misc/gen-id.js'; + +export class removeGroups1673892262930 { + name = 'removeGroups1673892262930'; + + async up(queryRunner) { + // migrate gallery posts into notes, keeping the ids + await queryRunner.query(` + INSERT INTO "note" ( + "id", "createdAt", "text", "cw", "userId", "visibility", "fileIds", "attachedFileTypes", "tags" + ) + WITH "file_types" ("id", "types") AS ( + SELECT "gallery_post"."id", ARRAY_AGG("drive_file"."type") + FROM "gallery_post" + JOIN "drive_file" ON "drive_file"."id" = ANY("gallery_post"."fileIds") + GROUP BY "gallery_post"."id" + ) + SELECT "gallery_post"."id", "gallery_post"."createdAt", + CASE + WHEN "gallery_post"."title" IS NULL THEN "gallery_post"."description" + ELSE '' || "gallery_post"."title" || E'\\n\\n' || "gallery_post"."description" + END, + CASE + WHEN "gallery_post"."isSensitive" THEN 'NSFW' + ELSE NULL + END, + "gallery_post"."userId", 'home', "gallery_post"."fileIds", "file_types"."types", "gallery_post"."tags" + FROM "gallery_post" + JOIN "file_types" ON "gallery_post"."id" = "file_types"."id" + `); + // make a clip for each users gallery + await queryRunner.query(`SELECT DISTINCT "userId" FROM "gallery_post"`).then(userIds => + Promise.all(userIds.map(({ userId }) => { + const clipId = genId(); + + // generate the clip itself + return queryRunner.query(`INSERT INTO "clip" ("id", "createdAt", "userId", "name", "isPublic") VALUES ($1, now(), $2, 'Gallery', true)`, [clipId, userId]) + // and add all the previous gallery posts to it + // to not have to use genId for each gallery post, we just prepend a zero, something that could never be generated by genId + .then(() => queryRunner.query(`INSERT INTO "clip_note" ("id", "noteId", "clipId") SELECT '0' || "id", "id", $1 FROM "gallery_post" WHERE "userId" = $2`, [clipId, userId])); + })) + ); + + await queryRunner.query(`DROP TABLE "gallery_like"`); + await queryRunner.query(`DROP TABLE "gallery_post"`); + } + + async down(queryRunner) { + // can only restore the table structure + await queryRunner.query(`CREATE TABLE "gallery_post" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "description" character varying(2048), "userId" character varying(32) NOT NULL, "fileIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "isSensitive" boolean NOT NULL DEFAULT false, "likedCount" integer NOT NULL DEFAULT '0', "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_8e90d7b6015f2c4518881b14753" PRIMARY KEY ("id")); COMMENT ON COLUMN "gallery_post"."createdAt" IS 'The created date of the GalleryPost.'; COMMENT ON COLUMN "gallery_post"."updatedAt" IS 'The updated date of the GalleryPost.'; COMMENT ON COLUMN "gallery_post"."userId" IS 'The ID of author.'; COMMENT ON COLUMN "gallery_post"."isSensitive" IS 'Whether the post is sensitive.'`); + await queryRunner.query(`CREATE INDEX "IDX_8f1a239bd077c8864a20c62c2c" ON "gallery_post" ("createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_f631d37835adb04792e361807c" ON "gallery_post" ("updatedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_985b836dddd8615e432d7043dd" ON "gallery_post" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_3ca50563facd913c425e7a89ee" ON "gallery_post" ("fileIds") `); + await queryRunner.query(`CREATE INDEX "IDX_f2d744d9a14d0dfb8b96cb7fc5" ON "gallery_post" ("isSensitive") `); + await queryRunner.query(`CREATE INDEX "IDX_1a165c68a49d08f11caffbd206" ON "gallery_post" ("likedCount") `); + await queryRunner.query(`CREATE INDEX "IDX_05cca34b985d1b8edc1d1e28df" ON "gallery_post" ("tags") `); + await queryRunner.query(`CREATE TABLE "gallery_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "postId" character varying(32) NOT NULL, CONSTRAINT "PK_853ab02be39b8de45cd720cc15f" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_8fd5215095473061855ceb948c" ON "gallery_like" ("userId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_df1b5f4099e99fb0bc5eae53b6" ON "gallery_like" ("userId", "postId") `); + await queryRunner.query(`ALTER TABLE "gallery_post" ADD CONSTRAINT "FK_985b836dddd8615e432d7043ddb" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "gallery_like" ADD CONSTRAINT "FK_8fd5215095473061855ceb948cf" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "gallery_like" ADD CONSTRAINT "FK_b1cb568bfe569e47b7051699fc8" FOREIGN KEY ("postId") REFERENCES "gallery_post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/migration/1674499888924-sync-orm.js b/packages/backend/migration/1674499888924-sync-orm.js new file mode 100644 index 000000000..12177b559 --- /dev/null +++ b/packages/backend/migration/1674499888924-sync-orm.js @@ -0,0 +1,23 @@ +export class syncOrm1674499888924 { + name = 'syncOrm1674499888924' + + async up(queryRunner) { + await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS 'The native access token of local users, or null.'`); + await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "UQ_8e001e5a101c6dca37df1a76d66"`); + + // remove human readable URL from notes where it is duplicated, so the index can be added + await queryRunner.query(`UPDATE "note" SET "url" = NULL WHERE "url" IN (SELECT "url" FROM "note" GROUP BY "url" HAVING COUNT("url") > 1)`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_71d35fceee0d0fa62b2fa8f3b2" ON "note" ("url") `); + + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9ecaed8c6dc43f3592c229282" ON "user_group_joining" ("userId", "userGroupId") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_d9ecaed8c6dc43f3592c229282"`); + + await queryRunner.query(`DROP INDEX "public"."IDX_71d35fceee0d0fa62b2fa8f3b2"`); + + await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "UQ_8e001e5a101c6dca37df1a76d66" UNIQUE ("accessTokenId")`); + await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS 'The native access token of the User. It will be null if the origin of the user is local.'`); + } +} diff --git a/packages/backend/migration/1675375940759-registry-remove-domain.js b/packages/backend/migration/1675375940759-registry-remove-domain.js new file mode 100644 index 000000000..50a6f6b8b --- /dev/null +++ b/packages/backend/migration/1675375940759-registry-remove-domain.js @@ -0,0 +1,19 @@ +export class registryRemoveDomain1675375940759 { + name = 'registryRemoveDomain1675375940759' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_0a72bdfcdb97c0eca11fe7ecad"`); + await queryRunner.query(`ALTER TABLE "registry_item" DROP COLUMN "domain"`); + await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "key" TYPE text USING "key"::text`); + // delete existing duplicated entries, keeping the latest updated one + await queryRunner.query(`DELETE FROM "registry_item" AS "a" WHERE "updatedAt" != (SELECT MAX("updatedAt") OVER (PARTITION BY "userId", "key", "scope") FROM "registry_item" AS "b" WHERE "a"."userId" = "b"."userId" AND "a"."key" = "b"."key" AND "a"."scope" = "b"."scope")`); + await queryRunner.query(`ALTER TABLE "registry_item" ADD CONSTRAINT "UQ_b8d6509f847331273ab99daccc7" UNIQUE ("userId", "key", "scope")`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "registry_item" DROP CONSTRAINT "UQ_b8d6509f847331273ab99daccc7"`); + await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "key" TYPE character varying(1024) USING "key"::varchar(1024)`); + await queryRunner.query(`ALTER TABLE "registry_item" ADD "domain" character varying(512)`); + await queryRunner.query(`CREATE INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad" ON "registry_item" ("domain") `); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 5d4eca072..197ddd605 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "backend", - "version": "13.0.0-preview3", + "version": "13.0.0-preview4", "main": "./index.js", "private": true, "type": "module", diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index 95286585d..a6431162d 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -26,7 +26,7 @@ const path = process.env.NODE_ENV === 'test' export default function load(): Config { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8')); - const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; + let config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; if (config.id && config.id !== 'aid') throw new Error('Unsupported ID algorithm. Only "aid" is supported.'); @@ -38,13 +38,30 @@ export default function load(): Config { config.port = config.port || parseInt(process.env.PORT || '', 10); + // set default values config.images = Object.assign({ info: '/twemoji/1f440.svg', notFound: '/twemoji/2049.svg', error: '/twemoji/1f480.svg', }, config.images ?? {}); - if (!config.maxNoteTextLength) config.maxNoteTextLength = 3000; + config.clusterLimits = Object.assign({ + web: 1, + queue: 1, + }, config.clusterLimits ?? {}); + + config = Object.assign({ + disableHsts: false, + deliverJobConcurrency: 128, + inboxJobConcurrency: 16, + deliverJobPerSec: -1, + inboxJobPerSec: 16, + deliverJobMaxAttempts: 12, + inboxJobMaxAttempts: 8, + proxyRemoteFiles: false, + maxFileSize: 262144000, // 250 MiB + maxNoteTextLength: 3000, + }, config); mixin.version = meta.version; mixin.host = url.host; @@ -60,21 +77,8 @@ export default function load(): Config { if (!config.redis.prefix) config.redis.prefix = mixin.host; - if (!config.clusterLimits) { - config.clusterLimits = { - web: 1, - queue: 1, - }; - } else { - config.clusterLimits = { - web: 1, - queue: 1, - ...config.clusterLimits, - }; - - if (config.clusterLimits.web < 1 || config.clusterLimits.queue < 1) { - throw new Error('invalid cluster limits'); - } + if (config.clusterLimits.web < 1 || config.clusterLimits.queue < 1) { + throw new Error('invalid cluster limits'); } return Object.assign(config, mixin); diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index 90b123ae8..b59142597 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -50,8 +50,6 @@ import { UserSecurityKey } from '@/models/entities/user-security-key.js'; import { AttestationChallenge } from '@/models/entities/attestation-challenge.js'; import { Page } from '@/models/entities/page.js'; import { PageLike } from '@/models/entities/page-like.js'; -import { GalleryPost } from '@/models/entities/gallery-post.js'; -import { GalleryLike } from '@/models/entities/gallery-like.js'; import { ModerationLog } from '@/models/entities/moderation-log.js'; import { UsedUsername } from '@/models/entities/used-username.js'; import { Announcement } from '@/models/entities/announcement.js'; @@ -143,8 +141,6 @@ export const entities = [ NoteUnread, Page, PageLike, - GalleryPost, - GalleryLike, DriveFile, DriveFolder, Poll, diff --git a/packages/backend/src/misc/api-permissions.ts b/packages/backend/src/misc/api-permissions.ts index 160cdf9fd..d7c115a50 100644 --- a/packages/backend/src/misc/api-permissions.ts +++ b/packages/backend/src/misc/api-permissions.ts @@ -27,9 +27,5 @@ export const kinds = [ 'write:user-groups', 'read:channels', 'write:channels', - 'read:gallery', - 'write:gallery', - 'read:gallery-likes', - 'write:gallery-likes', ]; // IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions). diff --git a/packages/backend/src/misc/download-url.ts b/packages/backend/src/misc/download-url.ts index 079e84e8a..694a80457 100644 --- a/packages/backend/src/misc/download-url.ts +++ b/packages/backend/src/misc/download-url.ts @@ -19,7 +19,6 @@ export async function downloadUrl(url: string, path: string): Promise { const timeout = 30 * SECOND; const operationTimeout = MINUTE; - const maxSize = config.maxFileSize || 262144000; const req = got.stream(url, { headers: { @@ -53,14 +52,14 @@ export async function downloadUrl(url: string, path: string): Promise { const contentLength = res.headers['content-length']; if (contentLength != null) { const size = Number(contentLength); - if (size > maxSize) { - logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`); + if (size > config.maxFileSize) { + logger.warn(`maxSize exceeded (${size} > ${config.maxFileSize}) on response`); req.destroy(); } } }).on('downloadProgress', (progress: Got.Progress) => { - if (progress.transferred > maxSize) { - logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`); + if (progress.transferred > config.maxFileSize) { + logger.warn(`maxSize exceeded (${progress.transferred} > ${config.maxFileSize}) on downloadProgress`); req.destroy(); } }); diff --git a/packages/backend/src/misc/fetch.ts b/packages/backend/src/misc/fetch.ts index 42eb445d9..6bd794e68 100644 --- a/packages/backend/src/misc/fetch.ts +++ b/packages/backend/src/misc/fetch.ts @@ -89,7 +89,7 @@ const _https = new https.Agent({ lookup: cache.lookup, } as https.AgentOptions); -const maxSockets = Math.max(256, config.deliverJobConcurrency || 128); +const maxSockets = Math.max(256, config.deliverJobConcurrency); /** * Get http proxy or non-proxy agent diff --git a/packages/backend/src/misc/renote.ts b/packages/backend/src/misc/renote.ts deleted file mode 100644 index cd51cd04a..000000000 --- a/packages/backend/src/misc/renote.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Note } from '@/models/entities/note.js'; - -export function isPureRenote(note: Note): note is Note & { renoteId: string, text: null, fileIds: null | never[], hasPoll: false } { - return note.renoteId != null - && note.text == null - && ( - note.fileIds == null - || note.fileIds.length === 0 - ) - && !note.hasPoll; -} diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index ad5dcb067..bad291b4a 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -28,7 +28,6 @@ import { packedAntennaSchema } from '@/models/schema/antenna.js'; import { packedClipSchema } from '@/models/schema/clip.js'; import { packedFederationInstanceSchema } from '@/models/schema/federation-instance.js'; import { packedQueueCountSchema } from '@/models/schema/queue.js'; -import { packedGalleryPostSchema } from '@/models/schema/gallery-post.js'; import { packedEmojiSchema } from '@/models/schema/emoji.js'; export const refs = { @@ -61,7 +60,6 @@ export const refs = { Antenna: packedAntennaSchema, Clip: packedClipSchema, FederationInstance: packedFederationInstanceSchema, - GalleryPost: packedGalleryPostSchema, Emoji: packedEmojiSchema, }; diff --git a/packages/backend/src/models/entities/access-token.ts b/packages/backend/src/models/entities/access-token.ts index b6dc8cebc..06a6cb00a 100644 --- a/packages/backend/src/models/entities/access-token.ts +++ b/packages/backend/src/models/entities/access-token.ts @@ -79,7 +79,6 @@ export class AccessToken { @Column('varchar', { length: 64, array: true, - default: '{}', }) public permission: string[]; diff --git a/packages/backend/src/models/entities/gallery-like.ts b/packages/backend/src/models/entities/gallery-like.ts deleted file mode 100644 index 259981392..000000000 --- a/packages/backend/src/models/entities/gallery-like.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { GalleryPost } from './gallery-post.js'; - -@Entity() -@Index(['userId', 'postId'], { unique: true }) -export class GalleryLike { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public postId: GalleryPost['id']; - - @ManyToOne(() => GalleryPost, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public post: GalleryPost | null; -} diff --git a/packages/backend/src/models/entities/gallery-post.ts b/packages/backend/src/models/entities/gallery-post.ts deleted file mode 100644 index 315bcd371..000000000 --- a/packages/backend/src/models/entities/gallery-post.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { DriveFile } from './drive-file.js'; - -@Entity() -export class GalleryPost { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the GalleryPost.', - }) - public createdAt: Date; - - @Index() - @Column('timestamp with time zone', { - comment: 'The updated date of the GalleryPost.', - }) - public updatedAt: Date; - - @Column('varchar', { - length: 256, - }) - public title: string; - - @Column('varchar', { - length: 2048, nullable: true, - }) - public description: string | null; - - @Index() - @Column({ - ...id(), - comment: 'The ID of author.', - }) - public userId: User['id']; - - @ManyToOne(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - array: true, default: '{}', - }) - public fileIds: DriveFile['id'][]; - - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the post is sensitive.', - }) - public isSensitive: boolean; - - @Index() - @Column('integer', { - default: 0, - }) - public likedCount: number; - - @Index() - @Column('varchar', { - length: 128, array: true, default: '{}', - }) - public tags: string[]; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index 1e5f418c5..e34dc53f7 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -117,6 +117,7 @@ export class Note { }) public uri: string | null; + @Index({ unique: true }) @Column('varchar', { length: 512, nullable: true, comment: 'The human readable url of a note. it will be null when the note is local.', diff --git a/packages/backend/src/models/entities/registry-item.ts b/packages/backend/src/models/entities/registry-item.ts index bbafae4a3..9ed2af720 100644 --- a/packages/backend/src/models/entities/registry-item.ts +++ b/packages/backend/src/models/entities/registry-item.ts @@ -1,9 +1,9 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, Unique } from 'typeorm'; import { id } from '../id.js'; import { User } from './user.js'; -// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい @Entity() +@Unique(['userId', 'key', 'scope']) export class RegistryItem { @PrimaryColumn(id()) public id: string; @@ -31,8 +31,7 @@ export class RegistryItem { @JoinColumn() public user: User | null; - @Column('varchar', { - length: 1024, + @Column('text', { comment: 'The key of the RegistryItem.', }) public key: string; @@ -48,11 +47,4 @@ export class RegistryItem { length: 1024, array: true, default: '{}', }) public scope: string[]; - - // サードパーティアプリに開放するときのためのカラム - @Index() - @Column('varchar', { - length: 512, nullable: true, - }) - public domain: string | null; } diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index 993f3e0f2..f4aa84d5b 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -42,8 +42,6 @@ import { UserSecurityKey } from './entities/user-security-key.js'; import { HashtagRepository } from './repositories/hashtag.js'; import { PageRepository } from './repositories/page.js'; import { PageLikeRepository } from './repositories/page-like.js'; -import { GalleryPostRepository } from './repositories/gallery-post.js'; -import { GalleryLikeRepository } from './repositories/gallery-like.js'; import { ModerationLogRepository } from './repositories/moderation-logs.js'; import { UsedUsername } from './entities/used-username.js'; import { ClipRepository } from './repositories/clip.js'; @@ -108,8 +106,6 @@ export const Signins = (SigninRepository); export const MessagingMessages = (MessagingMessageRepository); export const Pages = (PageRepository); export const PageLikes = (PageLikeRepository); -export const GalleryPosts = (GalleryPostRepository); -export const GalleryLikes = (GalleryLikeRepository); export const ModerationLogs = (ModerationLogRepository); export const Clips = (ClipRepository); export const ClipNotes = db.getRepository(ClipNote); diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index f75207134..ce3e7b705 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -108,9 +108,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ folderId: file.folderId, folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, { detail: true, - }) : null, - userId: opts.withUser ? file.userId : null, - user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null, + }) : undefined, + userId: file.userId, + user: (opts.withUser && file.userId) ? Users.pack(file.userId) : undefined, }); }, diff --git a/packages/backend/src/models/repositories/gallery-like.ts b/packages/backend/src/models/repositories/gallery-like.ts deleted file mode 100644 index 33f5b3ebb..000000000 --- a/packages/backend/src/models/repositories/gallery-like.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { GalleryLike } from '@/models/entities/gallery-like.js'; -import { GalleryPosts } from '../index.js'; - -export const GalleryLikeRepository = db.getRepository(GalleryLike).extend({ - async pack( - src: GalleryLike['id'] | GalleryLike, - me?: any, - ) { - const like = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: like.id, - post: await GalleryPosts.pack(like.post || like.postId, me), - }; - }, - - packMany( - likes: any[], - me: any, - ) { - return Promise.all(likes.map(x => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/gallery-post.ts b/packages/backend/src/models/repositories/gallery-post.ts deleted file mode 100644 index 7c54001ec..000000000 --- a/packages/backend/src/models/repositories/gallery-post.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { GalleryPost } from '@/models/entities/gallery-post.js'; -import { User } from '@/models/entities/user.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Users, DriveFiles, GalleryLikes } from '../index.js'; - -export const GalleryPostRepository = db.getRepository(GalleryPost).extend({ - async pack( - src: GalleryPost['id'] | GalleryPost, - me?: { id: User['id'] } | null | undefined, - ): Promise> { - const meId = me ? me.id : null; - const post = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: post.id, - createdAt: post.createdAt.toISOString(), - updatedAt: post.updatedAt.toISOString(), - userId: post.userId, - user: Users.pack(post.user || post.userId, me), - title: post.title, - description: post.description, - fileIds: post.fileIds, - files: DriveFiles.packMany(post.fileIds), - tags: post.tags.length > 0 ? post.tags : undefined, - isSensitive: post.isSensitive, - likedCount: post.likedCount, - isLiked: meId ? await GalleryLikes.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined, - }); - }, - - packMany( - posts: GalleryPost[], - me?: { id: User['id'] } | null | undefined, - ) { - return Promise.all(posts.map(x => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/schema/gallery-post.ts b/packages/backend/src/models/schema/gallery-post.ts deleted file mode 100644 index fc503d4a6..000000000 --- a/packages/backend/src/models/schema/gallery-post.ts +++ /dev/null @@ -1,69 +0,0 @@ -export const packedGalleryPostSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - updatedAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - title: { - type: 'string', - optional: false, nullable: false, - }, - description: { - type: 'string', - optional: false, nullable: true, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - fileIds: { - type: 'array', - optional: true, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - }, - files: { - type: 'array', - optional: true, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'DriveFile', - }, - }, - tags: { - type: 'array', - optional: true, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - isSensitive: { - type: 'boolean', - optional: false, nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 57de62a79..cc125a3de 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -96,7 +96,7 @@ export function deliver(user: ThinUser, content: unknown, to: string | null) { }; return deliverQueue.add(data, { - attempts: config.deliverJobMaxAttempts || 12, + attempts: config.deliverJobMaxAttempts, timeout: MINUTE, backoff: { type: 'apBackoff', @@ -113,7 +113,7 @@ export function inbox(activity: IActivity, signature: httpSignature.IParsedSigna }; return inboxQueue.add(data, { - attempts: config.inboxJobMaxAttempts || 8, + attempts: config.inboxJobMaxAttempts, timeout: 5 * MINUTE, backoff: { type: 'apBackoff', @@ -291,8 +291,8 @@ export function webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[ export default function() { if (envOption.onlyServer) return; - deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver); - inboxQueue.process(config.inboxJobConcurrency || 16, processInbox); + deliverQueue.process(config.deliverJobConcurrency, processDeliver); + inboxQueue.process(config.inboxJobConcurrency, processInbox); endedPollNotificationQueue.process(endedPollNotification); webhookDeliverQueue.process(64, processWebhookDeliver); processDb(dbQueue); diff --git a/packages/backend/src/queue/queues.ts b/packages/backend/src/queue/queues.ts index f3a267790..0f0f2f69d 100644 --- a/packages/backend/src/queue/queues.ts +++ b/packages/backend/src/queue/queues.ts @@ -4,8 +4,8 @@ import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPol export const systemQueue = initializeQueue>('system'); export const endedPollNotificationQueue = initializeQueue('endedPollNotification'); -export const deliverQueue = initializeQueue('deliver', config.deliverJobPerSec || 128); -export const inboxQueue = initializeQueue('inbox', config.inboxJobPerSec || 16); +export const deliverQueue = initializeQueue('deliver', config.deliverJobPerSec); +export const inboxQueue = initializeQueue('inbox', config.inboxJobPerSec); export const dbQueue = initializeQueue('db'); export const objectStorageQueue = initializeQueue('objectStorage'); export const webhookDeliverQueue = initializeQueue('webhookDeliver', 64); diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index 7f913de0b..dc1b47fef 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -62,9 +62,11 @@ export class DbResolver { id: parsed.id, }); } else { - return await Notes.findOneBy({ + return await Notes.findOneBy([{ uri: parsed.uri, - }); + }, { + url: parsed.uri, + }]); } } diff --git a/packages/backend/src/remote/activitypub/models/tag.ts b/packages/backend/src/remote/activitypub/models/tag.ts index 182a23765..ed277fc7c 100644 --- a/packages/backend/src/remote/activitypub/models/tag.ts +++ b/packages/backend/src/remote/activitypub/models/tag.ts @@ -39,11 +39,11 @@ export function extractQuoteUrl(tags: IObject | IObject[] | null | undefined): s 'https://www.w3.org/ns/activitystreams#quoteUrl', ].includes(rel) ) - ); + ) + // Deduplicate by href. + .filter((x, i, arr) => arr.findIndex(y => x.href === y.href) === i); if (quotes.length === 0) return null; - - // Deduplicate by href. // If there is more than one quote, we just pick the first/a random one. - quotes.filter((x, i, arr) => arr.findIndex(y => x.href === y.href) === i)[0].href; + else return quotes[0].href; } diff --git a/packages/backend/src/remote/activitypub/renderer/note-or-renote.ts b/packages/backend/src/remote/activitypub/renderer/note-or-renote.ts new file mode 100644 index 000000000..0857cd5b2 --- /dev/null +++ b/packages/backend/src/remote/activitypub/renderer/note-or-renote.ts @@ -0,0 +1,17 @@ +import * as foundkey from 'foundkey-js'; +import config from '@/config/index.js'; +import { Notes } from '@/models/index.js'; +import { Note } from '@/models/entities/note.js'; +import { IActivity } from '@/remote/activitypub/types.js'; +import renderNote from '@/remote/activitypub/renderer/note.js'; +import renderCreate from '@/remote/activitypub/renderer/create.js'; +import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; + +export async function renderNoteOrRenoteActivity(note: Note): Promise { + if (foundkey.entities.isPureRenote(note)) { + const renote = await Notes.findOneByOrFail({ id: note.renoteId }); + return renderAnnounce(renote.uri ?? `${config.url}/notes/${renote.id}`, note); + } else { + return renderCreate(await renderNote(note, false), note); + } +} diff --git a/packages/backend/src/remote/activitypub/renderer/person.ts b/packages/backend/src/remote/activitypub/renderer/person.ts index 7de957882..68f813aa8 100644 --- a/packages/backend/src/remote/activitypub/renderer/person.ts +++ b/packages/backend/src/remote/activitypub/renderer/person.ts @@ -30,12 +30,21 @@ export async function renderPerson(user: ILocalUser) { if (profile.fields) { for (const field of profile.fields) { + let value = field.value; + // try to parse it as a url + try { + if (field.value?.match(/^https?:/)) { + const url = new URL(field.value); + value = `${url.href}`; + } + } catch { + // guess it wasn't a url after all... + } + attachment.push({ type: 'PropertyValue', name: field.name, - value: (field.value != null && field.value.match(/^https?:/)) - ? `${new URL(field.value).href}` - : field.value, + value, }); } } diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 2dc4ea1e7..6bec78f8c 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -15,7 +15,8 @@ import { ILocalUser, User } from '@/models/entities/user.js'; import { renderLike } from '@/remote/activitypub/renderer/like.js'; import { getUserKeypair } from '@/misc/keypair-store.js'; import renderFollow from '@/remote/activitypub/renderer/follow.js'; -import Outbox, { packActivity } from './activitypub/outbox.js'; +import { renderNoteOrRenoteActivity } from '@/remote/activitypub/renderer/note-or-renote.js'; +import Outbox from './activitypub/outbox.js'; import Followers from './activitypub/followers.js'; import Following from './activitypub/following.js'; import Featured from './activitypub/featured.js'; @@ -115,7 +116,7 @@ router.get('/notes/:note/activity', async ctx => { return; } - ctx.body = renderActivity(await packActivity(note)); + ctx.body = renderActivity(await renderNoteOrRenoteActivity(note)); ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); }); diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts index 60e2ab711..a0a6af011 100644 --- a/packages/backend/src/server/activitypub/outbox.ts +++ b/packages/backend/src/server/activitypub/outbox.ts @@ -7,11 +7,11 @@ import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-c import renderNote from '@/remote/activitypub/renderer/note.js'; import renderCreate from '@/remote/activitypub/renderer/create.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; +import { renderNoteOrRenoteActivity } from '@/remote/activitypub/renderer/note-or-renote.js'; import { countIf } from '@/prelude/array.js'; import * as url from '@/prelude/url.js'; import { Users, Notes } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; -import { isPureRenote } from '@/misc/renote.js'; import { makePaginationQuery } from '../api/common/make-pagination-query.js'; import { setResponseType } from '../activitypub.js'; @@ -63,7 +63,7 @@ export default async (ctx: Router.RouterContext) => { if (sinceId) notes.reverse(); - const activities = await Promise.all(notes.map(note => packActivity(note))); + const activities = await Promise.all(notes.map(note => renderNoteOrRenoteActivity(note))); const rendered = renderOrderedCollectionPage( `${partOf}?${url.query({ page: 'true', @@ -94,16 +94,3 @@ export default async (ctx: Router.RouterContext) => { setResponseType(ctx); } }; - -/** - * Pack Create or Announce Activity - * @param note Note - */ -export async function packActivity(note: Note): Promise { - if (isPureRenote(note)) { - const renote = await Notes.findOneByOrFail({ id: note.renoteId }); - return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note); - } else { - return renderCreate(await renderNote(note, false), note); - } -} diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index 89493ce96..dc0e790bd 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -35,10 +35,8 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi limit.key = ep.name; } - // Rate limit - await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor).catch(() => { - throw new ApiError('RATE_LIMIT_EXCEEDED'); - }); + // Rate limit, may throw an ApiError + await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor); } if (ep.meta.requireCredential && user == null) { diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 59c4305c2..02f13d3a9 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -138,15 +138,6 @@ import * as ep___following_requests_accept from './endpoints/following/requests/ import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; import * as ep___following_requests_list from './endpoints/following/requests/list.js'; import * as ep___following_requests_reject from './endpoints/following/requests/reject.js'; -import * as ep___gallery_featured from './endpoints/gallery/featured.js'; -import * as ep___gallery_popular from './endpoints/gallery/popular.js'; -import * as ep___gallery_posts from './endpoints/gallery/posts.js'; -import * as ep___gallery_posts_create from './endpoints/gallery/posts/create.js'; -import * as ep___gallery_posts_delete from './endpoints/gallery/posts/delete.js'; -import * as ep___gallery_posts_like from './endpoints/gallery/posts/like.js'; -import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js'; -import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js'; -import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js'; import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js'; import * as ep___hashtags_list from './endpoints/hashtags/list.js'; import * as ep___hashtags_search from './endpoints/hashtags/search.js'; @@ -171,8 +162,6 @@ import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; import * as ep___i_favorites from './endpoints/i/favorites.js'; -import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; -import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js'; import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; import * as ep___i_importFollowing from './endpoints/i/import-following.js'; @@ -276,7 +265,6 @@ import * as ep___users from './endpoints/users.js'; import * as ep___users_clips from './endpoints/users/clips.js'; import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_following from './endpoints/users/following.js'; -import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_groups_create from './endpoints/users/groups/create.js'; import * as ep___users_groups_delete from './endpoints/users/groups/delete.js'; import * as ep___users_groups_invitations_accept from './endpoints/users/groups/invitations/accept.js'; @@ -446,15 +434,6 @@ const eps = [ ['following/requests/cancel', ep___following_requests_cancel], ['following/requests/list', ep___following_requests_list], ['following/requests/reject', ep___following_requests_reject], - ['gallery/featured', ep___gallery_featured], - ['gallery/popular', ep___gallery_popular], - ['gallery/posts', ep___gallery_posts], - ['gallery/posts/create', ep___gallery_posts_create], - ['gallery/posts/delete', ep___gallery_posts_delete], - ['gallery/posts/like', ep___gallery_posts_like], - ['gallery/posts/show', ep___gallery_posts_show], - ['gallery/posts/unlike', ep___gallery_posts_unlike], - ['gallery/posts/update', ep___gallery_posts_update], ['get-online-users-count', ep___getOnlineUsersCount], ['hashtags/list', ep___hashtags_list], ['hashtags/search', ep___hashtags_search], @@ -479,8 +458,6 @@ const eps = [ ['i/export-notes', ep___i_exportNotes], ['i/export-user-lists', ep___i_exportUserLists], ['i/favorites', ep___i_favorites], - ['i/gallery/likes', ep___i_gallery_likes], - ['i/gallery/posts', ep___i_gallery_posts], ['i/get-word-muted-notes-count', ep___i_getWordMutedNotesCount], ['i/import-blocking', ep___i_importBlocking], ['i/import-following', ep___i_importFollowing], @@ -584,7 +561,6 @@ const eps = [ ['users/clips', ep___users_clips], ['users/followers', ep___users_followers], ['users/following', ep___users_following], - ['users/gallery/posts', ep___users_gallery_posts], ['users/groups/create', ep___users_groups_create], ['users/groups/delete', ep___users_groups_delete], ['users/groups/invitations/accept', ep___users_groups_invitations_accept], @@ -717,6 +693,14 @@ export interface IEndpointMeta { * @example (v0) /api/notes/create -> /api/v2/notes */ readonly alias?: string; + + /** + * If any path parameters were used, they have to be listed here. + * Otherwise they will show up as query parameters in the documentation. + * + * Note: Path parameters cannot be optional. + */ + readonly pathParamers?: string[]; }; } diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index 3a051086a..676b6141d 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -7,6 +7,8 @@ export const meta = { requireCredential: true, + description: 'Tries to fetch the given `uri` from the remote server.', + limit: { duration: HOUR, max: 30, diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 414f71d31..4bf8f0091 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -18,6 +18,8 @@ export const meta = { requireCredential: true, + description: 'Shows the requested object. If necessary, fetches the object from the remote server.', + limit: { duration: HOUR, max: 30, diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts deleted file mode 100644 index baa70c14e..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { DAY } from '@/const.js'; -import { GalleryPosts } from '@/models/index.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: false, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'GalleryPost', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = GalleryPosts.createQueryBuilder('post') - .andWhere('post.createdAt > :date', { date: new Date(Date.now() - 3 * DAY) }) - .andWhere('post.likedCount > 0') - .orderBy('post.likedCount', 'DESC'); - - const posts = await query.take(10).getMany(); - - return await GalleryPosts.packMany(posts, me); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts deleted file mode 100644 index 552810e54..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/popular.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { GalleryPosts } from '@/models/index.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: false, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'GalleryPost', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = GalleryPosts.createQueryBuilder('post') - .andWhere('post.likedCount > 0') - .orderBy('post.likedCount', 'DESC'); - - const posts = await query.take(10).getMany(); - - return await GalleryPosts.packMany(posts, me); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts deleted file mode 100644 index 3a21afae1..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { GalleryPosts } from '@/models/index.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; - -export const meta = { - tags: ['gallery'], - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'GalleryPost', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) - .innerJoinAndSelect('post.user', 'user'); - - const posts = await query.take(ps.limit).getMany(); - - return await GalleryPosts.packMany(posts, me); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts deleted file mode 100644 index 230b7bec3..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { DriveFiles, GalleryPosts } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { GalleryPost } from '@/models/entities/gallery-post.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { HOUR } from '@/const.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true, - - kind: 'write:gallery', - - limit: { - duration: HOUR, - max: 300, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'GalleryPost', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - title: { type: 'string', minLength: 1 }, - description: { type: 'string', nullable: true }, - fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 32, items: { - type: 'string', format: 'misskey:id', - } }, - isSensitive: { type: 'boolean', default: false }, - }, - required: ['title', 'fileIds'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const files = (await Promise.all(ps.fileIds.map(fileId => - DriveFiles.findOneBy({ - id: fileId, - userId: user.id, - }), - ))).filter((file): file is DriveFile => file != null); - - if (files.length !== ps.fileIds.length) { - throw new ApiError( - 'INVALID_PARAM', - { - param: '#/properties/fileIds/items', - reason: 'contains invalid file IDs', - }, - ); - } - - const post = await GalleryPosts.insert(new GalleryPost({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), - title: ps.title, - description: ps.description, - userId: user.id, - isSensitive: ps.isSensitive, - fileIds: files.map(file => file.id), - })).then(x => GalleryPosts.findOneByOrFail(x.identifiers[0])); - - return await GalleryPosts.pack(post, user); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts deleted file mode 100644 index 65c0e62d0..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { GalleryPosts } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true, - - kind: 'write:gallery', - - errors: ['NO_SUCH_POST'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - postId: { type: 'string', format: 'misskey:id' }, - }, - required: ['postId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ - id: ps.postId, - userId: user.id, - }); - - if (post == null) throw new ApiError('NO_SUCH_POST'); - - await GalleryPosts.delete(post.id); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts deleted file mode 100644 index 1525d4dad..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { GalleryPosts, GalleryLikes } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true, - - kind: 'write:gallery-likes', - - errors: ['NO_SUCH_POST', 'ALREADY_LIKED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - postId: { type: 'string', format: 'misskey:id' }, - }, - required: ['postId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ id: ps.postId }); - if (post == null) throw new ApiError('NO_SUCH_POST'); - - // if already liked - const exist = await GalleryLikes.countBy({ - postId: post.id, - userId: user.id, - }); - - if (exist) throw new ApiError('ALREADY_LIKED'); - - // Create like - await GalleryLikes.insert({ - id: genId(), - createdAt: new Date(), - postId: post.id, - userId: user.id, - }); - - GalleryPosts.increment({ id: post.id }, 'likedCount', 1); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts deleted file mode 100644 index 640048028..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { GalleryPosts } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: false, - - errors: ['NO_SUCH_POST'], - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'GalleryPost', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - postId: { type: 'string', format: 'misskey:id' }, - }, - required: ['postId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const post = await GalleryPosts.findOneBy({ - id: ps.postId, - }); - - if (post == null) throw new ApiError('NO_SUCH_POST'); - - return await GalleryPosts.pack(post, me); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts deleted file mode 100644 index 61d71905e..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { GalleryPosts, GalleryLikes } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true, - - kind: 'write:gallery-likes', - - errors: ['NO_SUCH_POST', 'NOT_LIKED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - postId: { type: 'string', format: 'misskey:id' }, - }, - required: ['postId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ id: ps.postId }); - if (post == null) throw new ApiError('NO_SUCH_POST'); - - const exist = await GalleryLikes.findOneBy({ - postId: post.id, - userId: user.id, - }); - - if (exist == null) throw new ApiError('NOT_LIKED'); - - // Delete like - await GalleryLikes.delete(exist.id); - - GalleryPosts.decrement({ id: post.id }, 'likedCount', 1); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts deleted file mode 100644 index 071229f89..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { DriveFiles, GalleryPosts } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { HOUR } from '@/const.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true, - - kind: 'write:gallery', - - limit: { - duration: HOUR, - max: 300, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'GalleryPost', - }, - - errors: ['INVALID_PARAM'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - postId: { type: 'string', format: 'misskey:id' }, - title: { type: 'string', minLength: 1 }, - description: { type: 'string', nullable: true }, - fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 32, items: { - type: 'string', format: 'misskey:id', - } }, - isSensitive: { type: 'boolean', default: false }, - }, - required: ['postId', 'title', 'fileIds'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const files = (await Promise.all(ps.fileIds.map(fileId => - DriveFiles.findOneBy({ - id: fileId, - userId: user.id, - }), - ))).filter((file): file is DriveFile => file != null); - - if (files.length !== ps.fileIds.length) { - throw new ApiError( - 'INVALID_PARAM', - { - param: '#/properties/fileIds/items', - reason: 'contains invalid file IDs', - }, - ); - } - - await GalleryPosts.update({ - id: ps.postId, - userId: user.id, - }, { - updatedAt: new Date(), - title: ps.title, - description: ps.description, - isSensitive: ps.isSensitive, - fileIds: files.map(file => file.id), - }); - - const post = await GalleryPosts.findOneByOrFail({ id: ps.postId }); - - return await GalleryPosts.pack(post, user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts deleted file mode 100644 index f5ceddd43..000000000 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { GalleryLikes } from '@/models/index.js'; -import define from '../../../define.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; - -export const meta = { - tags: ['account', 'gallery'], - - requireCredential: true, - - kind: 'read:gallery-likes', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - post: { - type: 'object', - optional: false, nullable: false, - ref: 'GalleryPost', - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(GalleryLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId) - .andWhere('like.userId = :meId', { meId: user.id }) - .leftJoinAndSelect('like.post', 'post'); - - const likes = await query - .take(ps.limit) - .getMany(); - - return await GalleryLikes.packMany(likes, user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts deleted file mode 100644 index 2ecd47f1b..000000000 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { GalleryPosts } from '@/models/index.js'; -import define from '../../../define.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; - -export const meta = { - tags: ['account', 'gallery'], - - requireCredential: true, - - kind: 'read:gallery', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'GalleryPost', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) - .andWhere('post.userId = :meId', { meId: user.id }); - - const posts = await query - .take(ps.limit) - .getMany(); - - return await GalleryPosts.packMany(posts, user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index d92bc7af9..21fda2607 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -20,7 +20,6 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') .andWhere('item.userId = :userId', { userId: user.id }) .andWhere('item.scope = :scope', { scope: ps.scope }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index 7addd0fab..2c3f7887f 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -24,7 +24,6 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') .andWhere('item.userId = :userId', { userId: user.id }) .andWhere('item.key = :key', { key: ps.key }) .andWhere('item.scope = :scope', { scope: ps.scope }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index a9ca60485..02e31e626 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -24,7 +24,6 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') .andWhere('item.userId = :userId', { userId: user.id }) .andWhere('item.key = :key', { key: ps.key }) .andWhere('item.scope = :scope', { scope: ps.scope }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index d9982b036..e865ce1db 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -20,7 +20,6 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') .andWhere('item.userId = :userId', { userId: user.id }) .andWhere('item.scope = :scope', { scope: ps.scope }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index b625f50ca..3072805bc 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -21,7 +21,6 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const query = RegistryItems.createQueryBuilder('item') .select('item.key') - .where('item.domain IS NULL') .andWhere('item.userId = :userId', { userId: user.id }) .andWhere('item.scope = :scope', { scope: ps.scope }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index f656f3d83..b75fbaff6 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -24,7 +24,6 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') .andWhere('item.userId = :userId', { userId: user.id }) .andWhere('item.key = :key', { key: ps.key }) .andWhere('item.scope = :scope', { scope: ps.scope }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts index d946fe609..9b0f43d33 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts @@ -17,7 +17,6 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const query = RegistryItems.createQueryBuilder('item') .select('item.scope') - .where('item.domain IS NULL') .andWhere('item.userId = :userId', { userId: user.id }); const items = await query.getMany(); diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 03d81d8c4..98eab9b35 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -24,7 +24,6 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const query = RegistryItems.createQueryBuilder('item') - .where('item.domain IS NULL') .andWhere('item.userId = :userId', { userId: user.id }) .andWhere('item.key = :key', { key: ps.key }) .andWhere('item.scope = :scope', { scope: ps.scope }); @@ -42,7 +41,6 @@ export default define(meta, paramDef, async (ps, user) => { createdAt: new Date(), updatedAt: new Date(), userId: user.id, - domain: null, scope: ps.scope, key: ps.key, value: ps.value, diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 091bab766..db270af3a 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -25,6 +25,7 @@ export const meta = { v2: { method: 'get', alias: 'notes/:noteId/children', + pathParameters: ['noteId'], }, } as const; diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 3cd878800..e87daedcf 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -22,6 +22,7 @@ export const meta = { v2: { method: 'get', alias: 'notes/:noteId/clips', + pathParameters: ['noteId'], }, errors: ['NO_SUCH_NOTE'], diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index 7e736c736..3ea80f860 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -22,6 +22,7 @@ export const meta = { v2: { method: 'get', alias: 'notes/:noteId/conversation', + pathParameters: ['noteId'], }, errors: ['NO_SUCH_NOTE'], diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 1b272e9dc..d10ba4df0 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -1,5 +1,5 @@ import { In } from 'typeorm'; -import { noteVisibilities } from 'foundkey-js'; +import { noteVisibilities, entities } from 'foundkey-js'; import create from '@/services/note/create.js'; import { User } from '@/models/entities/user.js'; import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js'; @@ -7,7 +7,6 @@ import { DriveFile } from '@/models/entities/drive-file.js'; import { Note } from '@/models/entities/note.js'; import { Channel } from '@/models/entities/channel.js'; import { HOUR } from '@/const.js'; -import { isPureRenote } from '@/misc/renote.js'; import config from '@/config/index.js'; import { ApiError } from '../../error.js'; import define from '../../define.js'; @@ -160,7 +159,7 @@ export default define(meta, paramDef, async (ps, user) => { throw e; }); - if (isPureRenote(renote)) throw new ApiError('PURE_RENOTE', 'Cannot renote a pure renote.'); + if (entities.isPureRenote(renote)) throw new ApiError('PURE_RENOTE', 'Cannot renote a pure renote.'); // check that the visibility is not less restrictive if (noteVisibilities.indexOf(renote.visibility) > noteVisibilities.indexOf(ps.visibility)) { @@ -185,7 +184,7 @@ export default define(meta, paramDef, async (ps, user) => { throw e; }); - if (isPureRenote(reply)) throw new ApiError('PURE_RENOTE', 'Cannot reply to a pure renote.'); + if (entities.isPureRenote(reply)) throw new ApiError('PURE_RENOTE', 'Cannot reply to a pure renote.'); // check that the visibility is not less restrictive if (noteVisibilities.indexOf(reply.visibility) > noteVisibilities.indexOf(ps.visibility)) { diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 3d8af2c7c..83f768c37 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -14,13 +14,15 @@ export const meta = { limit: { duration: HOUR, - max: 300, - minInterval: SECOND, + max: 30, + minInterval: 10 * SECOND, + key: 'delete', }, v2: { method: 'delete', alias: 'notes/:noteId', + pathParameters: ['noteId'], }, errors: ['ACCESS_DENIED', 'NO_SUCH_NOTE'], diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 6fd0e8f16..2b73a381d 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -25,7 +25,8 @@ export const meta = { v2: { method: 'get', - alias: 'notes/:noteId/reactions/:type?', + alias: 'notes/:noteId/reactions', + pathParameters: ['noteId'], }, errors: ['NO_SUCH_NOTE'], diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index b974fc3ef..51786b446 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -13,8 +13,9 @@ export const meta = { limit: { duration: HOUR, - max: 60, - minInterval: 3 * SECOND, + max: 30, + minInterval: 10 * SECOND, + key: 'delete', }, errors: ['NO_SUCH_NOTE', 'NOT_REACTED'], diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 55674f9ee..af02aa15c 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -25,6 +25,7 @@ export const meta = { v2: { method: 'get', alias: 'notes/:noteId/renotes', + pathParameters: ['noteId'], }, errors: ['NO_SUCH_NOTE'], diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 34b679ae4..e3bb33047 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -25,6 +25,7 @@ export const meta = { v2: { method: 'get', alias: 'notes/:noteId/replies', + pathParameters: ['noteId'], }, errors: ['NO_SUCH_NOTE'], diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 6c3f68407..9f3c68f32 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -17,6 +17,7 @@ export const meta = { v2: { method: 'get', alias: 'notes/:noteId', + pathParameters: ['noteId'], }, errors: ['NO_SUCH_NOTE'], diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index c578e1419..a98903177 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -30,6 +30,7 @@ export const meta = { v2: { method: 'get', alias: 'notes/:noteId/status', + pathParameters: ['noteId'], }, errors: ['NO_SUCH_NOTE'], diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 1c4989354..e366f84dd 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -57,7 +57,8 @@ export const meta = { v2: { method: 'get', - alias: 'notes/:noteId/translate/:targetLang/:sourceLang?', + alias: 'notes/:noteId/translate/:targetLang', + pathParameters: ['noteId', 'targetLang'], }, errors: ['NO_SUCH_NOTE'], diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 20f3aa429..446a14981 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -14,13 +14,15 @@ export const meta = { limit: { duration: HOUR, - max: 300, - minInterval: SECOND, + max: 30, + minInterval: 10 * SECOND, + key: 'delete', }, v2: { method: 'delete', alias: 'notes/:noteId/renotes', + pathParameters: ['noteId'], }, errors: ['NO_SUCH_NOTE'], diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts deleted file mode 100644 index 8184c4ce4..000000000 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { GalleryPosts } from '@/models/index.js'; -import define from '../../../define.js'; -import { makePaginationQuery } from '../../../common/make-pagination-query.js'; - -export const meta = { - tags: ['users', 'gallery'], - - description: 'Show all gallery posts by the given user.', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'GalleryPost', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) - .andWhere('post.userId = :userId', { userId: ps.userId }); - - const posts = await query - .take(ps.limit) - .getMany(); - - return await GalleryPosts.packMany(posts, user); -}); diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index c015c7608..7636ab0ba 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -29,8 +29,16 @@ export class ApiError extends Error { */ public apply(ctx: Koa.Context, endpoint: string): void { ctx.status = this.httpStatusCode; - if (ctx.status === 401) { - ctx.response.set('WWW-Authenticate', 'Bearer'); + // set additional headers + switch (ctx.status) { + case 401: + ctx.response.set('WWW-Authenticate', 'Bearer'); + break; + case 429: + if (typeof this.info === 'object' && typeof this.info.reset === 'number') { + ctx.respose.set('Retry-After', Math.floor(this.info.reset - (Date.now() / 1000))); + } + break; } ctx.body = { error: { @@ -73,7 +81,7 @@ export const errors: Record httpStatusCode: 409, }, ALREADY_LIKED: { - message: 'You already liked that page or gallery post.', + message: 'You already liked that page.', httpStatusCode: 409, }, ALREADY_MUTING: { @@ -292,10 +300,6 @@ export const errors: Record message: 'No such parent folder.', httpStatusCode: 404, }, - NO_SUCH_POST: { - message: 'No such gallery post.', - httpStatusCode: 404, - }, NO_SUCH_RESET_REQUEST: { message: 'No such password reset request.', httpStatusCode: 404, @@ -337,7 +341,7 @@ export const errors: Record httpStatusCode: 409, }, NOT_LIKED: { - message: 'You have not liked that page or gallery post.', + message: 'You have not liked that page.', httpStatusCode: 409, }, NOT_MUTING: { diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts index 2b835d446..2780afff3 100644 --- a/packages/backend/src/server/api/limiter.ts +++ b/packages/backend/src/server/api/limiter.ts @@ -2,11 +2,12 @@ import Limiter from 'ratelimiter'; import Logger from '@/services/logger.js'; import { redisClient } from '@/db/redis.js'; import { IEndpointMeta } from './endpoints.js'; +import { ApiError } from './error.js'; const logger = new Logger('limiter'); -export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string) => new Promise((ok, reject) => { - if (process.env.NODE_ENV === 'test') ok(); +export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string) => new Promise((resolve) => { + if (process.env.NODE_ENV === 'test') resolve(); const hasShortTermLimit = typeof limitation.minInterval === 'number'; @@ -19,10 +20,10 @@ export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable< } else if (hasLongTermLimit) { max(); } else { - ok(); + resolve(); } - // Short-term limit + // Short-term limit, calls long term limit if appropriate. function min(): void { const minIntervalLimiter = new Limiter({ id: `${actor}:${limitation.key}:min`, @@ -33,18 +34,19 @@ export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable< minIntervalLimiter.get((err, info) => { if (err) { - return reject('ERR'); + logger.error(err); + throw new ApiError('INTERNAL_ERROR'); } logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); if (info.remaining === 0) { - reject('BRIEF_REQUEST_INTERVAL'); + throw new ApiError('RATE_LIMIT_EXCEEDED', info); } else { if (hasLongTermLimit) { max(); } else { - ok(); + resolve(); } } }); @@ -61,15 +63,16 @@ export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable< limiter.get((err, info) => { if (err) { - return reject('ERR'); + logger.error(err); + throw new ApiError('INTERNAL_ERROR'); } logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); if (info.remaining === 0) { - reject('RATE_LIMIT_EXCEEDED'); + throw new ApiError('RATE_LIMIT_EXCEEDED', info); } else { - ok(); + resolve(); } }); } diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 3643887f7..ece0bef24 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -126,11 +126,21 @@ export function genOpenapiSpec() { }; } - let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; + let desc = endpoint.meta.description ?? 'No description provided.'; desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; if (endpoint.meta.kind) { - const kind = endpoint.meta.kind; - desc += ` / **Permission**: *${kind}*`; + desc += '\n\n**Permission**: `' + endpoint.meta.kind + '`'; + } + if (endpoint.meta.limit) { + const limit = endpoint.meta.limit; + + desc += '\n### Rate limit\nRate limiting group: `' + (limit.key ?? endpoint.name) + '`'; + if (limit.duration && limit.max) { + desc += ` \nNo more than ${limit.max} requests every ${limit.duration} ms.`; + } + if (limit.minInterval) { + desc += ` \nMinimum delay between each request: ${endpoint.meta.limit.minInterval} ms.`; + } } const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json'; @@ -183,6 +193,7 @@ export function genOpenapiSpec() { }, }, responses, + deprecated: endpoint.meta.stability === 'deprecated', }; const path = { @@ -209,11 +220,37 @@ export function genOpenapiSpec() { spec.paths['/' + endpoint.name] = path; if (endpoint.meta.v2) { + const route = `/v2/${endpoint.meta.v2.alias ?? endpoint.name.replace(/-/g, '_')}`; // we need a clone of the API endpoint info because otherwise we change it by reference const infoClone = structuredClone(info); - const route = `/v2/${endpoint.meta.v2.alias ?? endpoint.name.replace(/-/g, '_')}`; + // fix the way parameters are passed + const hasBody = !(endpoint.meta.v2.method === 'get' || endpoint.meta.v2.method === 'delete'); + if (!hasBody) { + // these methods do not (usually) have a body + delete infoClone.requestBody; + infoClone.parameters = []; + for (const name in schema.properties) { + infoClone.parameters.push({ + name, + in: endpoint.meta.v2?.pathParameters?.includes(name) ? 'path' : 'query', + schema: schema.properties[name], + required: endpoint.meta.v2?.pathParameters?.includes(name) || schema.required?.includes(name) || false, + }); + } + } else if (endpoint.meta.v2.pathParameters) { + for (const name in endpoint.meta.v2.pathParameters) { + delete infoClone.requestBody.content[requestType].schema.properties[name]; + infoClone.parameters.push({ + name, + in: 'path', + schema: schema.properties[name], + required: true, + }); + } + } - infoClone['operationId'] = infoClone['summary'] = route; + infoClone['operationId'] = endpoint.meta.v2.method.toUpperCase() + '/' + route; + infoClone['summary'] = endpoint.meta.v2.method.toUpperCase() + ' ' + route; spec.paths[route] = { ...spec.paths[route], diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index b2bfabe52..f910cb3c3 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -25,13 +25,8 @@ export default async (ctx: Koa.Context) => { new ApiError(e, info).apply(ctx, 'signin'); } - try { - // not more than 1 attempt per second and not more than 10 attempts per hour - await limiter({ key: 'signin', duration: HOUR, max: 10, minInterval: SECOND }, getIpHash(ctx.ip)); - } catch (err) { - error('RATE_LIMIT_EXCEEDED'); - return; - } + // not more than 1 attempt per second and not more than 10 attempts per hour + await limiter({ key: 'signin', duration: HOUR, max: 10, minInterval: SECOND }, getIpHash(ctx.ip)); if (typeof username !== 'string') { error('INVALID_PARAM', { param: 'username', reason: 'not a string' }); diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts index 825896b56..797443afb 100644 --- a/packages/backend/src/server/api/streaming.ts +++ b/packages/backend/src/server/api/streaming.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'events'; import * as http from 'node:http'; import { WebSocketServer } from 'ws'; -import { MINUTE } from '@/const.js'; +import { SECOND, MINUTE } from '@/const.js'; import { subscriber as redisClient } from '@/db/redis.js'; import { Users } from '@/models/index.js'; import { Connection } from './stream/index.js'; @@ -43,6 +43,20 @@ export const initializeStreamingServer = (server: http.Server): void => { const main = new Connection(socket, ev, user, app); + // ping/pong mechanism + let pingTimeout = null; + function startHeartbeat() { + if (pingTimeout) clearTimeout(pingTimeout); + + socket.ping(); + pingTimeout = setTimeout(() => { + socket.terminate(); + }, 30 * SECOND); + } + startHeartbeat(); + socket.on('ping', () => { startHeartbeat(); }); + socket.on('pong', () => { startHeartbeat(); }); + // keep user "online" while a stream is connected const intervalId = user ? setInterval(() => { Users.update(user.id, { @@ -54,19 +68,13 @@ export const initializeStreamingServer = (server: http.Server): void => { lastActiveDate: new Date(), }); } + socket.once('close', () => { ev.removeAllListeners(); main.dispose(); redisClient.off('message', onRedisMessage); if (intervalId) clearInterval(intervalId); - }); - - // ping/pong mechanism - // TODO: the websocket protocol already specifies a ping/pong mechanism, why is this necessary? - socket.on('message', async (data) => { - if (data.type === 'utf8' && data.utf8Data === 'ping') { - socket.send('pong'); - } + if (pingTimeout) clearTimeout(pingTimeout); }); }); }); diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 3776c24f2..fec6663fa 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -18,7 +18,7 @@ import { KoaAdapter } from '@bull-board/koa'; import { In, IsNull } from 'typeorm'; import { fetchMeta } from '@/misc/fetch-meta.js'; import config from '@/config/index.js'; -import { Users, Notes, UserProfiles, Pages, Channels, Clips, GalleryPosts } from '@/models/index.js'; +import { Users, Notes, UserProfiles, Pages, Channels, Clips, DriveFiles } from '@/models/index.js'; import * as Acct from '@/misc/acct.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; import { queues } from '@/queue/queues.js'; @@ -324,15 +324,75 @@ router.get('/notes/:note', async (ctx, next) => { if (note) { try { // FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774) - const _note = await Notes.pack(note); + const packedNote = await Notes.pack(note); const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); const meta = await fetchMeta(); + + // If the note has a CW (is sensitive as a whole) or any of the files is sensitive or there are no + // files, they are not used for a preview. + let filesOpengraph = []; + if (!packedNote.cw || packedNote.files.length > 0 || packedNote.files.every(file => !file.isSensitive)) { + let limit = 4; + for (const file of packedNote.files) { + if (file.type.startsWith('image/')) { + filesOpengraph.push([ + "og:image", + DriveFiles.getPublicUrl(file, true), + ]); + filesOpengraph.push([ + "og:image:type", + file.type, + ]); + if (file.properties != null) { + filesOpengraph.push([ + "og:image:width", + file.properties?.width, + ]); + filesOpengraph.push([ + "og:image:height", + file.properties?.height, + ]); + } + if (file.comment) { + filesOpengraph.push([ + "og:image:alt", + file.comment, + ]); + } + } else if (file.type.startsWith('audio/')) { + filesOpengraph.push([ + "og:audio", + DriveFiles.getPublicUrl(file), + ]); + filesOpengraph.push([ + "og:audio:type", + file.type, + ]); + } else if (file.type.startsWith('video/')) { + filesOpengraph.push([ + "og:video", + DriveFiles.getPublicUrl(file), + ]); + filesOpengraph.push([ + "og:video:type", + file.type, + ]); + } else { + // doesn't count towards the limit + continue; + } + + // limit the number of presented attachments + if (--limit < 0) break; + } + } + await ctx.render('note', { - note: _note, + note: packedNote, profile, - avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: note.userId })), + filesOpengraph, // TODO: Let locale changeable by instance setting - summary: getNoteSummary(_note), + summary: getNoteSummary(packedNote), instanceName: meta.name || 'FoundKey', icon: meta.iconUrl, themeColor: meta.themeColor, @@ -421,31 +481,6 @@ router.get('/clips/:clip', async (ctx, next) => { await next(); }); -// Gallery post -router.get('/gallery/:post', async (ctx, next) => { - const post = await GalleryPosts.findOneBy({ id: ctx.params.post }); - - if (post) { - const _post = await GalleryPosts.pack(post); - const profile = await UserProfiles.findOneByOrFail({ userId: post.userId }); - const meta = await fetchMeta(); - await ctx.render('gallery-post', { - post: _post, - profile, - avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: post.userId })), - instanceName: meta.name || 'FoundKey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - ctx.set('Cache-Control', 'public, max-age=15'); - - return; - } - - await next(); -}); - // Channel router.get('/channels/:channel', async (ctx, next) => { const channel = await Channels.findOneBy({ diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug index 4c692bf59..12e402616 100644 --- a/packages/backend/src/server/web/views/clip.pug +++ b/packages/backend/src/server/web/views/clip.pug @@ -12,20 +12,12 @@ block desc meta(name='description' content= clip.description) block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) - meta(property='og:description' content= clip.description) - meta(property='og:url' content= url) - meta(property='og:image' content= avatarUrl) + meta(property='og:type' content='website') + meta(property='og:title' content=title) + meta(property='og:description' content=clip.description) + meta(property='og:url' content=url) + meta(property='og:image' content=avatarUrl) block meta if profile.noCrawle meta(name='robots' content='noindex') - - meta(name='misskey:user-username' content=user.username) - meta(name='misskey:user-id' content=user.id) - meta(name='misskey:clip-id' content=clip.id) - - // todo - if user.twitter - meta(name='twitter:creator' content=`@${user.twitter.screenName}`) diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug deleted file mode 100644 index ca0663a48..000000000 --- a/packages/backend/src/server/web/views/gallery-post.pug +++ /dev/null @@ -1,33 +0,0 @@ -extends ./base - -block vars - - const user = post.user; - - const title = post.title; - - const url = `${config.url}/gallery/${post.id}`; - -block title - = `${title} | ${instanceName}` - -block desc - meta(name='description' content= post.description) - -block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) - meta(property='og:description' content= post.description) - meta(property='og:url' content= url) - meta(property='og:image' content= post.files[0].thumbnailUrl) - -block meta - if user.host || profile.noCrawle - meta(name='robots' content='noindex') - - meta(name='misskey:user-username' content=user.username) - meta(name='misskey:user-id' content=user.id) - - // todo - if user.twitter - meta(name='twitter:creator' content=`@${user.twitter.screenName}`) - - if !user.host - link(rel='alternate' href=url type='application/activity+json') diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index 65696ea13..02b61f572 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -13,24 +13,19 @@ block desc meta(name='description' content= summary) block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) + meta(property='og:type' content='article') + meta(property='og:article:published_time' content=note.createdAt) + meta(property='og:article:author:username' content=user.username) + meta(property='og:title' content= title) meta(property='og:description' content= summary) - meta(property='og:url' content= url) - meta(property='og:image' content= avatarUrl) + meta(property='og:url' content= url) + for opengraphTag in filesOpengraph + meta(property=opengraphTag[0] content=opengraphTag[1]) block meta if user.host || isRenote || profile.noCrawle meta(name='robots' content='noindex') - meta(name='misskey:user-username' content=user.username) - meta(name='misskey:user-id' content=user.id) - meta(name='misskey:note-id' content=note.id) - - // todo - if user.twitter - meta(name='twitter:creator' content=`@${user.twitter.screenName}`) - if note.prev link(rel='prev' href=`${config.url}/notes/${note.prev}`) if note.next diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug index 4219e76a5..54859e53e 100644 --- a/packages/backend/src/server/web/views/page.pug +++ b/packages/backend/src/server/web/views/page.pug @@ -12,20 +12,13 @@ block desc meta(name='description' content= page.summary) block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) - meta(property='og:description' content= page.summary) - meta(property='og:url' content= url) - meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl) + meta(property='og:type' content='article') + meta(property='og:article:author:username' content=user.username) + meta(property='og:title' content=title) + meta(property='og:description' content=page.summary) + meta(property='og:url' content=url) + meta(property='og:image' content=page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl) block meta if profile.noCrawle meta(name='robots' content='noindex') - - meta(name='misskey:user-username' content=user.username) - meta(name='misskey:user-id' content=user.id) - meta(name='misskey:page-id' content=page.id) - - // todo - if user.twitter - meta(name='twitter:creator' content=`@${user.twitter.screenName}`) diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug index 119993fdb..dfcae89ec 100644 --- a/packages/backend/src/server/web/views/user.pug +++ b/packages/backend/src/server/web/views/user.pug @@ -11,19 +11,16 @@ block desc meta(name='description' content= profile.description) block og - meta(property='og:type' content='blog') - meta(property='og:title' content= title) + meta(property='og:type' content='profile') + meta(property='og:profile:username' content=user.username) meta(property='og:description' content= profile.description) - meta(property='og:url' content= url) - meta(property='og:image' content= avatarUrl) + meta(property='og:url' content=url) + meta(property='og:image' content=avatarUrl) block meta if user.host || profile.noCrawle meta(name='robots' content='noindex') - meta(name='misskey:user-username' content=user.username) - meta(name='misskey:user-id' content=user.id) - if profile.twitter meta(name='twitter:creator' content=`@${profile.twitter.screenName}`) diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 67f5e0d57..8f48bc12a 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -36,6 +36,7 @@ import { Cache } from '@/misc/cache.js'; import { UserProfile } from '@/models/entities/user-profile.js'; import { getActiveWebhooks } from '@/misc/webhook-cache.js'; import { IActivity } from '@/remote/activitypub/type.js'; +import { renderNoteOrRenoteActivity } from '@/remote/activitypub/renderer/note-or-renote.js'; import { MINUTE } from '@/const.js'; import { updateHashtags } from '../update-hashtag.js'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; @@ -428,9 +429,9 @@ export default async (user: { id: User['id']; username: User['username']; host: }); //#region AP deliver - if (Users.isLocalUser(user)) { + if (Users.isLocalUser(user) && !data.localOnly) { (async () => { - const noteActivity = await renderNoteOrRenoteActivity(data, note); + const noteActivity = renderActivity(await renderNoteOrRenoteActivity(note)); const dm = new DeliverManager(user, noteActivity); // Delivered to remote users who have been mentioned @@ -487,16 +488,6 @@ export default async (user: { id: User['id']; username: User['username']; host: index(note); }); -async function renderNoteOrRenoteActivity(data: Option, note: Note): Promise { - if (data.localOnly) return null; - - const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0) - ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note) - : renderCreate(await renderNote(note, false), note); - - return renderActivity(content); -} - function incRenoteCount(renote: Note): void { Notes.createQueryBuilder().update() .set({ diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index 6075f15b1..1df08d5ea 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -1,4 +1,5 @@ import { FindOptionsWhere, In, IsNull, Not } from 'typeorm'; +import * as foundkey from 'foundkey-js'; import { publishNoteStream } from '@/services/stream.js'; import renderDelete from '@/remote/activitypub/renderer/delete.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; @@ -12,7 +13,6 @@ import { Notes, Users, Instances } from '@/models/index.js'; import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js'; import { DeliverManager } from '@/remote/activitypub/deliver-manager.js'; import { countSameRenotes } from '@/misc/count-same-renotes.js'; -import { isPureRenote } from '@/misc/renote.js'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; import { deliverToRelays } from '../relay.js'; @@ -42,7 +42,7 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us let renote: Note | null = null; // if deleted note is renote - if (isPureRenote(note)) { + if (foundkey.entities.isPureRenote(note)) { renote = await Notes.findOneBy({ id: note.renoteId }); } diff --git a/packages/backend/src/services/send-email.ts b/packages/backend/src/services/send-email.ts index b4dd0d828..2a2df2d9b 100644 --- a/packages/backend/src/services/send-email.ts +++ b/packages/backend/src/services/send-email.ts @@ -8,7 +8,7 @@ export const logger = new Logger('email'); export async function sendEmail(to: string, subject: string, html: string, text: string): Promise { const meta = await fetchMeta(true); - const iconUrl = `${config.url}/static-assets/mi-white.png`; + const iconUrl = `${config.url}/static-assets/icons/512.png`; const emailSettingUrl = `${config.url}/settings/email`; const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; diff --git a/packages/client/package.json b/packages/client/package.json index d7cbe0b0f..d9a994d4a 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "13.0.0-preview3", + "version": "13.0.0-preview4", "private": true, "scripts": { "watch": "vite build --watch --mode development", diff --git a/packages/client/src/components/cw-button.vue b/packages/client/src/components/cw-button.vue index 17299586c..c68552ab0 100644 --- a/packages/client/src/components/cw-button.vue +++ b/packages/client/src/components/cw-button.vue @@ -9,7 +9,6 @@ import { computed } from 'vue'; import { length } from 'stringz'; import * as foundkey from 'foundkey-js'; -import { concat } from '@/scripts/array'; import { i18n } from '@/i18n'; const props = defineProps<{ @@ -22,11 +21,12 @@ const emit = defineEmits<{ }>(); const label = computed(() => { - return concat([ + return [ props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [], props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length }) ] : [], props.note.poll != null ? [i18n.ts.poll] : [], - ] as string[][]).join(' / '); + props.note.renoteId != null ? [i18n.ts.quote] : [], + ].flat().join(' / '); }); const toggle = () => { diff --git a/packages/client/src/components/gallery-post-preview.vue b/packages/client/src/components/gallery-post-preview.vue deleted file mode 100644 index 6acd9646b..000000000 --- a/packages/client/src/components/gallery-post-preview.vue +++ /dev/null @@ -1,113 +0,0 @@ - - - - - diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index 73b297ab1..ef083eb4d 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -4,7 +4,6 @@ import MkUrl from '@/components/global/url.vue'; import MkLink from '@/components/link.vue'; import MkMention from '@/components/mention.vue'; import MkEmoji from '@/components/global/emoji.vue'; -import { concat } from '@/scripts/array'; import MkFormula from '@/components/formula.vue'; import MkCode from '@/components/code.vue'; import MkSearch from '@/components/mfm-search.vue'; @@ -31,10 +30,6 @@ export default defineComponent({ type: Object, default: null, }, - i: { - type: Object, - default: null, - }, customEmojis: { required: false, }, @@ -54,7 +49,7 @@ export default defineComponent({ return t.match(/^[0-9.]+s$/) ? t : null; }; - const genEl = (ast: mfm.MfmNode[]) => concat(ast.map((token): VNode[] => { + const genEl = (ast: mfm.MfmNode[]) => ast.map((token): VNode[] => { switch (token.type) { case 'text': { const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); @@ -318,7 +313,7 @@ export default defineComponent({ return []; } } - })); + }).flat(); // Parse ast to DOM return h('span', genEl(ast)); diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index e64e13375..3549ef953 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -159,12 +159,7 @@ if (noteViewInterruptors.length > 0) { }); } -const isRenote = ( - note.renote != null && - note.text == null && - note.fileIds.length === 0 && - note.poll == null -); +const isRenote = foundkey.entities.isPureRenote(note); const el = ref(); const menuButton = ref(); diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index f818f7dae..b0938b3c0 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -148,12 +148,7 @@ if (noteViewInterruptors.length > 0) { }); } -const isRenote = ( - note.renote != null && - note.text == null && - note.fileIds.length === 0 && - note.poll == null -); +const isRenote = foundkey.entities.isPureRenote(note); const el = ref(); const menuButton = ref(); diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index d929d8d32..3d5434096 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -207,7 +207,7 @@ const maxTextLength = $computed((): number => { const canPost = $computed((): boolean => { return !posting && - (1 <= textLength || 1 <= files.length || !!poll || !!props.renote) && + (1 <= textLength || 1 <= files.length || !!poll || !!props.renote || !!quoteId) && (textLength <= maxTextLength) && (!poll || poll.choices.length >= 2); }); @@ -573,7 +573,7 @@ async function post() { text: text === '' ? undefined : text, fileIds: files.length > 0 ? files.map(f => f.id) : undefined, replyId: props.reply ? props.reply.id : undefined, - renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined, + renoteId: props.renote?.id ?? quoteId ?? undefined, channelId: props.channel ? props.channel.id : undefined, poll, cw: useCw ? cw || '' : undefined, diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts index b730fdd8e..1de63b4dc 100644 --- a/packages/client/src/menu.ts +++ b/packages/client/src/menu.ts @@ -131,11 +131,6 @@ export const menuDef = reactive({ icon: 'fas fa-file-alt', to: '/pages', }, - gallery: { - title: 'gallery', - icon: 'fas fa-icons', - to: '/gallery', - }, clips: { title: 'clip', icon: 'fas fa-paperclip', diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue deleted file mode 100644 index 964f6d4ff..000000000 --- a/packages/client/src/pages/gallery/edit.vue +++ /dev/null @@ -1,153 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue deleted file mode 100644 index 2e2a72897..000000000 --- a/packages/client/src/pages/gallery/index.vue +++ /dev/null @@ -1,137 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue deleted file mode 100644 index 2886321d2..000000000 --- a/packages/client/src/pages/gallery/post.vue +++ /dev/null @@ -1,264 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/gallery.vue b/packages/client/src/pages/user/gallery.vue deleted file mode 100644 index 1d053b279..000000000 --- a/packages/client/src/pages/user/gallery.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue index 3b47bb91a..24c88e75d 100644 --- a/packages/client/src/pages/user/index.vue +++ b/packages/client/src/pages/user/index.vue @@ -8,7 +8,6 @@ - @@ -33,7 +32,6 @@ const XHome = defineAsyncComponent(() => import('./home.vue')); const XReactions = defineAsyncComponent(() => import('./reactions.vue')); const XClips = defineAsyncComponent(() => import('./clips.vue')); const XPages = defineAsyncComponent(() => import('./pages.vue')); -const XGallery = defineAsyncComponent(() => import('./gallery.vue')); const props = withDefaults(defineProps<{ acct: string; @@ -82,10 +80,6 @@ const headerTabs = $computed(() => [{ key: 'pages', title: i18n.ts.pages, icon: 'fas fa-file-alt', -}, { - key: 'gallery', - title: i18n.ts.gallery, - icon: 'fas fa-icons', }]); definePageMetadata(computed(() => user ? { diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index 791076e71..10869a3cf 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -123,20 +123,6 @@ export const routes = [{ }, { path: '/pages', component: page(() => import('./pages/pages.vue')), -}, { - path: '/gallery/:postId/edit', - component: page(() => import('./pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/new', - component: page(() => import('./pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/:postId', - component: page(() => import('./pages/gallery/post.vue')), -}, { - path: '/gallery', - component: page(() => import('./pages/gallery/index.vue')), }, { path: '/channels/:channelId/edit', component: page(() => import('./pages/channel-editor.vue')), diff --git a/packages/client/src/scripts/array.ts b/packages/client/src/scripts/array.ts index 26c6195d6..e5b9016c0 100644 --- a/packages/client/src/scripts/array.ts +++ b/packages/client/src/scripts/array.ts @@ -15,19 +15,12 @@ export function count(a: T, xs: T[]): number { return countIf(x => x === a, xs); } -/** - * Concatenate an array of arrays - */ -export function concat(xss: T[][]): T[] { - return ([] as T[]).concat(...xss); -} - /** * Intersperse the element between the elements of the array * @param sep The element to be interspersed */ export function intersperse(sep: T, xs: T[]): T[] { - return concat(xs.map(x => [sep, x])).slice(1); + return xs.map(x => [sep, x]).flat().slice(1); } /** diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 57445a49a..1afab0071 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -16,14 +16,9 @@ export function getNoteMenu(props: { isDeleted: Ref; currentClipPage?: Ref; }) { - const isRenote = ( - props.note.renote != null && - props.note.text == null && - props.note.fileIds.length === 0 && - props.note.poll == null - ); - - const appearNote = isRenote ? props.note.renote as foundkey.entities.Note : props.note; + const appearNote = foundkey.entities.isPureRenote(props.note) + ? props.note.renote as foundkey.entities.Note + : props.note; function del(): void { os.confirm({ diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue index 3cec9b388..c7121d9d0 100644 --- a/packages/client/src/ui/_common_/common.vue +++ b/packages/client/src/ui/_common_/common.vue @@ -33,14 +33,6 @@ const dev: Ref = ref(_DEV_); const onNotification = (notification: { type: string; id: any; }): void => { if ($i?.mutingNotificationTypes.includes(notification.type)) return; - // if push notifications are enabled there is no need to pass the notification along - if (!instance.enableServiceWorker) { - // service worker is not enabled or set up on the server, pass the notification along - navigator.serviceWorker.ready.then(registration => { - registration.active.postMessage({ type: 'notification', body: notification }); - }); - } - sound.play('notification'); }; diff --git a/packages/foundkey-js/package.json b/packages/foundkey-js/package.json index feaa68965..de0862919 100644 --- a/packages/foundkey-js/package.json +++ b/packages/foundkey-js/package.json @@ -1,6 +1,6 @@ { "name": "foundkey-js", - "version": "13.0.0-preview3", + "version": "13.0.0-preview4", "description": "Fork of misskey-js for Foundkey", "type": "module", "main": "./built/index.js", diff --git a/packages/foundkey-js/src/api.types.ts b/packages/foundkey-js/src/api.types.ts index 8798113a5..64a5f98aa 100644 --- a/packages/foundkey-js/src/api.types.ts +++ b/packages/foundkey-js/src/api.types.ts @@ -1,5 +1,5 @@ import { - Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, GalleryPost, Instance, InstanceMetadata, + Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, Instance, InstanceMetadata, LiteInstanceMetadata, MeDetailed, Note, NoteFavorite, OriginType, Page, ServerInfo, Stats, User, UserDetailed, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage, @@ -282,15 +282,6 @@ export type Endpoints = { 'following/requests/cancel': { req: { userId: User['id'] }; res: User; }; 'following/requests/list': { req: NoParams; res: FollowRequest[]; }; 'following/requests/reject': { req: { userId: User['id'] }; res: null; }; - 'gallery/featured': { req: TODO; res: TODO; }; - 'gallery/popular': { req: TODO; res: TODO; }; - 'gallery/posts': { req: TODO; res: TODO; }; - 'gallery/posts/create': { req: TODO; res: TODO; }; - 'gallery/posts/delete': { req: { postId: GalleryPost['id'] }; res: null; }; - 'gallery/posts/like': { req: TODO; res: TODO; }; - 'gallery/posts/show': { req: TODO; res: TODO; }; - 'gallery/posts/unlike': { req: TODO; res: TODO; }; - 'gallery/posts/update': { req: TODO; res: TODO; }; 'get-online-users-count': { req: TODO; res: TODO; }; 'hashtags/list': { req: TODO; res: TODO; }; 'hashtags/search': { req: TODO; res: TODO; }; @@ -315,8 +306,6 @@ export type Endpoints = { 'i/export-notes': { req: TODO; res: TODO; }; 'i/export-user-lists': { req: TODO; res: TODO; }; 'i/favorites': { req: { limit?: number; sinceId?: NoteFavorite['id']; untilId?: NoteFavorite['id']; }; res: NoteFavorite[]; }; - 'i/gallery/likes': { req: TODO; res: TODO; }; - 'i/gallery/posts': { req: TODO; res: TODO; }; 'i/get-word-muted-notes-count': { req: TODO; res: TODO; }; 'i/import-blocking': { req: TODO; res: TODO; }; 'i/import-following': { req: TODO; res: TODO; }; @@ -488,7 +477,6 @@ export type Endpoints = { 'users/clips': { req: TODO; res: TODO; }; 'users/followers': { req: { userId?: User['id']; username?: User['username']; host?: User['host'] | null; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFollowerPopulated[]; }; 'users/following': { req: { userId?: User['id']; username?: User['username']; host?: User['host'] | null; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFolloweePopulated[]; }; - 'users/gallery/posts': { req: TODO; res: TODO; }; 'users/groups/create': { req: TODO; res: TODO; }; 'users/groups/delete': { req: { groupId: UserGroup['id'] }; res: null; }; 'users/groups/invitations/accept': { req: TODO; res: TODO; }; diff --git a/packages/foundkey-js/src/consts.ts b/packages/foundkey-js/src/consts.ts index 645bdf223..a90b8720a 100644 --- a/packages/foundkey-js/src/consts.ts +++ b/packages/foundkey-js/src/consts.ts @@ -35,8 +35,4 @@ export const permissions = [ 'write:user-groups', 'read:channels', 'write:channels', - 'read:gallery', - 'write:gallery', - 'read:gallery-likes', - 'write:gallery-likes', ]; diff --git a/packages/foundkey-js/src/entities.ts b/packages/foundkey-js/src/entities.ts index c92e60082..30dd90582 100644 --- a/packages/foundkey-js/src/entities.ts +++ b/packages/foundkey-js/src/entities.ts @@ -126,8 +126,6 @@ export type DriveFile = { export type DriveFolder = TODO; -export type GalleryPost = TODO; - export type Note = { id: ID; createdAt: DateString; @@ -473,3 +471,14 @@ export type UserSorting = | '+updatedAt' | '-updatedAt'; export type OriginType = 'combined' | 'local' | 'remote'; + +export function isPureRenote(note: Note): boolean { + return note.renoteId != null + && note.text == null + && note.cw == null + && ( + note.fileIds == null + || note.fileIds.length === 0 + ) + && note.poll == null; +} diff --git a/packages/sw/package.json b/packages/sw/package.json index 2c3c26ffe..41bf30605 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -1,6 +1,6 @@ { "name": "sw", - "version": "13.0.0-preview3", + "version": "13.0.0-preview4", "private": true, "scripts": { "watch": "node build.js watch", diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 490c40b30..10cfc69d3 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -164,9 +164,6 @@ self.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message' case 'initialize': swLang.setLang(ev.data.lang); break; - case 'notification': - createNotification(ev.data); - break; } } })());