diff --git a/.dockerignore b/.dockerignore index 9ed558a25..fd28d7d47 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,4 @@ .autogen -.github -.travis .vscode .config Dockerfile @@ -12,4 +10,3 @@ elasticsearch/ node_modules/ redis/ files/ -misskey-assets/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ef053b53..09a3a91e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -139,6 +139,14 @@ To generate the changelog, we use a standard shortlog command: `git shortlog --f The person performing the release process should build the next CHANGELOG section based on this output, not use it as-is. Full releases should also remove any pre-release CHANGELOG sections. +Here is the step by step checklist: +1. If **stable** release, announce the comment period. Restart the comment period if a blocker bug is found and fixed. +2. Edit various `package.json`s to the new version. +3. Write a new entry into the changelog. + You should use the `git shortlog --format='%h %s' --group=trailer:changelog LAST_TAG..` command to get general data, + then rewrite it in a human way. +4. Tag the commit with the changes in 2 and 3 (if together, else the latter). + ## Translation [![Translation status](http://translate.akkoma.dev/widgets/foundkey/-/svg-badge.svg)](http://translate.akkoma.dev/engage/foundkey/) @@ -289,8 +297,11 @@ PostgreSQL array indices **start at 1**. When `IN` is performed on a column that may contain `NULL` values, use `OR` or similar to handle `NULL` values. ### creating migrations -In `packages/backend`, run: +First make changes to the entity files in `packages/backend/src/models/entities/`. + +Then, in `packages/backend`, run: ```sh +yarn build npx typeorm migration:generate -d ormconfig.js -o ``` diff --git a/COPYING b/COPYING index 13c13bf93..e6d1222b6 100644 --- a/COPYING +++ b/COPYING @@ -1,10 +1,10 @@ Unless otherwise stated this repository is -Copyright © 2014-2020 syuilo and contributers - +Copyright © 2014-2022 syuilo and contributors +Copyright © 2022 FoundKey contributors And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. -Misskey includes several third-party Open-Source softwares. +FoundKey includes several third-party Open-Source softwares. Emoji keywords for Unicode 11 and below by Mu-An Chiou License: MIT diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 37e20a83a..1effff76a 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -8,9 +8,10 @@ This guide will also assume you're using Debian or a derivative like Ubuntu. If FoundKey requires the following packages to run: ### Dependencies :package: -* **[Node.js](https://nodejs.org/en/)** (16.x/18.x) +* **[Node.js](https://nodejs.org/en/)** (18.x) * **[PostgreSQL](https://www.postgresql.org/)** (12.x minimum; 13.x+ is preferred) * **[Redis](https://redis.io/)** +* **[Yarn](https://yarnpkg.com/)** The following are needed to compile native npm modules: * A C/C++ compiler like **GCC** or **Clang** @@ -18,17 +19,16 @@ The following are needed to compile native npm modules: * **[Python](https://python.org/)** (3.x) ### Optional -* [Yarn](https://yarnpkg.com/) - *If you decide not to install it, use `npx yarn` instead of `yarn`.* * [FFmpeg](https://www.ffmpeg.org/) To install the dependiencies on Debian (or derivatives like Ubuntu) you can use the following commands: ```sh -curl -fsSL https://deb.nodesource.com/setup_16.x | bash - +curl -fsSL https://deb.nodesource.com/setup_18.x | bash - apt install build-essential python3 nodejs postgresql redis +corepack enable # for yarn # Optional dependencies apt install ffmpeg -corepack enable # for yarn ``` ## Create FoundKey user diff --git a/docs/migrating.md b/docs/migrating.md index 1d2ea5573..772dcf80c 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -40,6 +40,9 @@ git merge tags/v13.0.0-preview2 --squash # you are now on the "next" release ``` +## Making sure modern Yarn works +Foundkey uses Modern Yarn instead of Classic (1.x). To make sure the `yarn` command will work going forward, run `corepack enable`. + ## Rebuilding and running database migrations This will be pretty much the same as a regular update of Misskey. Note that `yarn install` may take a while since dependency versions have been updated or removed and we use a newer version of Yarn. ```sh diff --git a/locales/en-US.yml b/locales/en-US.yml index 1f0cb466a..920d6e2cd 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -167,7 +167,6 @@ general: "General" wallpaper: "Wallpaper" setWallpaper: "Set wallpaper" removeWallpaper: "Remove wallpaper" -searchWith: "Search: {q}" youHaveNoLists: "You don't have any lists" followConfirm: "Are you sure that you want to follow {name}?" proxyAccount: "Proxy account" @@ -190,7 +189,9 @@ charts: "Charts" perHour: "Per Hour" perDay: "Per Day" stopActivityDelivery: "Stop sending activities" +stopActivityDeliveryDescription: "Local activities will not be sent to this instance. Receiving activities works as before." blockThisInstance: "Block this instance" +blockThisInstanceDescription: "Local activites will not be sent to this instance. Activites from this instance will be discarded." operations: "Operations" software: "Software" version: "Version" @@ -797,6 +798,8 @@ onlineStatus: "Online status" hideOnlineStatus: "Hide online status" hideOnlineStatusDescription: "Hiding your online status reduces the convenience of\ \ some features such as the search." +federateBlocks: "Federate blocks" +federateBlocksDescription: "If disabled, block activities won't be sent." online: "Online" active: "Active" offline: "Offline" @@ -875,7 +878,7 @@ ffVisibility: "Follows/Followers Visibility" ffVisibilityDescription: "Allows you to configure who can see who you follow and who\ \ follows you." continueThread: "View thread continuation" -deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" +deleteAccountConfirm: "This will irreversibly delete the account {handle}. Proceed?" incorrectPassword: "Incorrect password." voteConfirm: "Confirm your vote for \"{choice}\"?" hide: "Hide" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6e821d051..22d0eebbd 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -740,6 +740,8 @@ unknown: "不明" onlineStatus: "オンライン状態" hideOnlineStatus: "オンライン状態を隠す" hideOnlineStatusDescription: "オンライン状態を隠すと、検索などの一部機能において利便性が低下することがあります。" +federateBlocks: "ブロックを連合に送信" +federateBlocksDescription: "オフにするとBlockのActivityは連合に送信しません" online: "オンライン" active: "アクティブ" offline: "オフライン" diff --git a/packages/backend/migration/1631880003000-user-block-federation.js b/packages/backend/migration/1631880003000-user-block-federation.js new file mode 100644 index 000000000..1a90d5ae4 --- /dev/null +++ b/packages/backend/migration/1631880003000-user-block-federation.js @@ -0,0 +1,12 @@ +export class userBlockFederation1631880003000 { + name = 'userBlockFederation1631880003000'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "federateBlocks" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "federateBlocks"`); + } + +} diff --git a/packages/backend/migration/1657570176749-remove-ads.ts b/packages/backend/migration/1657570176749-remove-ads.js similarity index 94% rename from packages/backend/migration/1657570176749-remove-ads.ts rename to packages/backend/migration/1657570176749-remove-ads.js index 03a4168aa..585581730 100644 --- a/packages/backend/migration/1657570176749-remove-ads.ts +++ b/packages/backend/migration/1657570176749-remove-ads.js @@ -1,5 +1,5 @@ export class removeAds1657570176749 { - name = 'removeAds1657570176749' + name = 'removeAds1657570176749'; async up(queryRunner) { await queryRunner.query(`DROP TABLE "ad"`); diff --git a/packages/backend/migration/1667503570994-sync.js b/packages/backend/migration/1667503570994-sync.js new file mode 100644 index 000000000..97dfa2d2c --- /dev/null +++ b/packages/backend/migration/1667503570994-sync.js @@ -0,0 +1,44 @@ +export class sync1667503570994 { + name = 'sync1667503570994' + + async up(queryRunner) { + await Promise.all([ + // the migration for renote mutes added the index to the wrong table + queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`), + queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`), + queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`), + queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `), + queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `), + queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `), + + queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`), + queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`), + queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`), + queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" SET NOT NULL`), + queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" SET DEFAULT ''`), + queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `), + queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`), + queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`), + ]); + } + + async down(queryRunner) { + await Promise.all([ + queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`), + queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`), + queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`), + queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" DROP NOT NULL`), + queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`), + queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`), + queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`), + + queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`), + queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`), + queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`), + queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `), + queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `), + queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `), + ]); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 51ce022cc..863aa4ac9 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -15,8 +15,8 @@ "test": "npm run mocha" }, "dependencies": { - "@bull-board/api": "^4.2.2", - "@bull-board/koa": "4.0.0", + "@bull-board/api": "^4.3.1", + "@bull-board/koa": "^4.3.1", "@discordapp/twemoji": "14.0.2", "@elastic/elasticsearch": "7.11.0", "@koa/cors": "3.1.0", @@ -96,7 +96,7 @@ "rss-parser": "3.12.0", "sanitize-html": "2.7.0", "semver": "7.3.7", - "sharp": "0.30.7", + "sharp": "0.31.2", "speakeasy": "2.0.0", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", @@ -158,6 +158,7 @@ "@types/sanitize-html": "2.6.2", "@types/semver": "7.3.12", "@types/sharp": "0.30.5", + "@types/sinon": "^10.0.13", "@types/sinonjs__fake-timers": "8.1.2", "@types/speakeasy": "2.0.7", "@types/tinycolor2": "1.4.3", @@ -173,6 +174,7 @@ "eslint-plugin-import": "^2.26.0", "execa": "6.1.0", "form-data": "^4.0.0", + "sinon": "^14.0.2", "typescript": "^4.8.3" } } diff --git a/packages/backend/src/config/redis.ts b/packages/backend/src/config/redis.ts index b234ff689..5b7d51c32 100644 --- a/packages/backend/src/config/redis.ts +++ b/packages/backend/src/config/redis.ts @@ -10,7 +10,7 @@ function getRedisFamily(family?: string | number): number { dual: 0, }; if (typeof family === 'string' && family in familyMap) { - return familyMap[family]; + return familyMap[family as keyof typeof familyMap]; } else if (typeof family === 'number' && Object.values(familyMap).includes(family)) { return family; } diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 7c804d175..ff49fb04e 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -24,7 +24,7 @@ export type Source = { db?: number; prefix?: string; }; - elasticsearch: { + elasticsearch?: { host: string; port: number; ssl?: boolean; diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index 7a56127e5..8dcefc319 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -3,6 +3,9 @@ export const SECOND = 1000; export const MINUTE = 60 * SECOND; export const HOUR = 60 * MINUTE; export const DAY = 24 * HOUR; +export const WEEK = 7 * DAY; +export const MONTH = 30 * DAY; +export const YEAR = 365 * DAY; export const USER_ONLINE_THRESHOLD = 10 * MINUTE; export const USER_ACTIVE_THRESHOLD = 3 * DAY; diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts index e3211cc25..011265ad7 100644 --- a/packages/backend/src/mfm/from-html.ts +++ b/packages/backend/src/mfm/from-html.ts @@ -62,22 +62,21 @@ export function fromHtml(html: string, hashtagNames?: string[]): string { const rel = node.attrs.find(x => x.name === 'rel'); const href = node.attrs.find(x => x.name === 'href'); - // ハッシュタグ + // hashtags if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { text += txt; - // メンション + // mentions } else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { const part = txt.split('@'); if (part.length === 2 && href) { - //#region ホスト名部分が省略されているので復元する + // restore the host name part const acct = `${txt}@${(new URL(href.value)).hostname}`; text += acct; - //#endregion } else if (part.length === 3) { text += txt; } - // その他 + // other } else { const generateLink = () => { if (!href && !txt) { diff --git a/packages/backend/src/misc/antenna-cache.ts b/packages/backend/src/misc/antenna-cache.ts index 97249c146..6523d89b6 100644 --- a/packages/backend/src/misc/antenna-cache.ts +++ b/packages/backend/src/misc/antenna-cache.ts @@ -5,7 +5,7 @@ import { subscriber } from '@/db/redis.js'; let antennasFetched = false; let antennas: Antenna[] = []; -export async function getAntennas() { +export async function getAntennas(): Promise { if (!antennasFetched) { antennas = await Antennas.find(); antennasFetched = true; diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index e5b911ed3..e472acd38 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,10 +1,12 @@ export class Cache { public cache: Map; private lifetime: number; + public fetcher: (key: string | null) => Promise; - constructor(lifetime: Cache['lifetime']) { + constructor(lifetime: number, fetcher: Cache['fetcher']) { this.cache = new Map(); this.lifetime = lifetime; + this.fetcher = fetcher; } public set(key: string | null, value: T): void { @@ -17,64 +19,38 @@ export class Cache { public get(key: string | null): T | undefined { const cached = this.cache.get(key); if (cached == null) return undefined; + + // discard if past the cache lifetime if ((Date.now() - cached.date) > this.lifetime) { this.cache.delete(key); return undefined; } + return cached.value; } - public delete(key: string | null) { + public delete(key: string | null): void { this.cache.delete(key); } /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * If the value is cached, it is returned. Otherwise the fetcher is + * run to get the value. If the fetcher returns undefined, it is + * returned but not cached. */ - public async fetch(key: string | null, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { - const cachedValue = this.get(key); - if (cachedValue !== undefined) { - if (validator) { - if (validator(cachedValue)) { - // Cache HIT - return cachedValue; - } - } else { - // Cache HIT - return cachedValue; + public async fetch(key: string | null): Promise { + const cached = this.get(key); + if (cached !== undefined) { + return cached; + } else { + const value = await this.fetcher(key); + + // don't cache undefined + if (value !== undefined) { + this.set(key, value); } - } - // Cache MISS - const value = await fetcher(); - this.set(key, value); - return value; - } - - /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします - */ - public async fetchMaybe(key: string | null, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { - const cachedValue = this.get(key); - if (cachedValue !== undefined) { - if (validator) { - if (validator(cachedValue)) { - // Cache HIT - return cachedValue; - } - } else { - // Cache HIT - return cachedValue; - } + return value; } - - // Cache MISS - const value = await fetcher(); - if (value !== undefined) { - this.set(key, value); - } - return value; } } diff --git a/packages/backend/src/misc/captcha.ts b/packages/backend/src/misc/captcha.ts index 1431a4d80..4f0e757e7 100644 --- a/packages/backend/src/misc/captcha.ts +++ b/packages/backend/src/misc/captcha.ts @@ -3,7 +3,7 @@ import fetch from 'node-fetch'; import config from '@/config/index.js'; import { getAgentByUrl } from './fetch.js'; -export async function verifyRecaptcha(secret: string, response: string) { +export async function verifyRecaptcha(secret: string, response: string): Promise { const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => { throw new Error(`recaptcha-request-failed: ${e.message}`); }); @@ -14,7 +14,7 @@ export async function verifyRecaptcha(secret: string, response: string) { } } -export async function verifyHcaptcha(secret: string, response: string) { +export async function verifyHcaptcha(secret: string, response: string): Promise { const result = await getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => { throw new Error(`hcaptcha-request-failed: ${e.message}`); }); diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts index aa9247b41..e563d749e 100644 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ b/packages/backend/src/misc/check-hit-antenna.ts @@ -3,22 +3,26 @@ import { Note } from '@/models/entities/note.js'; import { User } from '@/models/entities/user.js'; import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js'; import * as Acct from '@/misc/acct.js'; +import { MINUTE } from '@/const.js'; import { getFullApAccount } from './convert-host.js'; import { Packed } from './schema.js'; import { Cache } from './cache.js'; -const blockingCache = new Cache(1000 * 60 * 5); +const blockingCache = new Cache( + 5 * MINUTE, + (blockerId) => Blockings.findBy({ blockerId }).then(res => res.map(x => x.blockeeId)), +); -// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている +// designation for users you follow, list users and groups is disabled for performance reasons /** - * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい + * either noteUserFollowers or antennaUserFollowing must be specified */ export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise { if (note.visibility === 'specified') return false; - // アンテナ作成者がノート作成者にブロックされていたらスキップ - const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); + // skip if the antenna creator is blocked by the note author + const blockings = await blockingCache.fetch(noteUser.id); if (blockings.some(blocking => blocking === antenna.userId)) return false; if (note.visibility === 'followers') { diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts index d7662820a..c49e95ace 100644 --- a/packages/backend/src/misc/check-word-mute.ts +++ b/packages/backend/src/misc/check-word-mute.ts @@ -5,6 +5,7 @@ import { User } from '@/models/entities/user.js'; type NoteLike = { userId: Note['userId']; text: Note['text']; + cw: Note['cw']; }; type UserLike = { diff --git a/packages/backend/src/misc/convert-host.ts b/packages/backend/src/misc/convert-host.ts index aa771af14..705edaedd 100644 --- a/packages/backend/src/misc/convert-host.ts +++ b/packages/backend/src/misc/convert-host.ts @@ -11,12 +11,12 @@ export function isSelfHost(host: string | null): boolean { return toPuny(config.host) === toPuny(host); } -export function extractDbHost(uri: string) { +export function extractDbHost(uri: string): string { const url = new URL(uri); return toPuny(url.hostname); } -export function toPuny(host: string) { +export function toPuny(host: string): string { return toASCII(host.toLowerCase()); } diff --git a/packages/backend/src/misc/detect-url-mime.ts b/packages/backend/src/misc/detect-url-mime.ts index cd143cf2f..bb4049c2e 100644 --- a/packages/backend/src/misc/detect-url-mime.ts +++ b/packages/backend/src/misc/detect-url-mime.ts @@ -2,7 +2,7 @@ import { createTemp } from './create-temp.js'; import { downloadUrl } from './download-url.js'; import { detectType } from './get-file-info.js'; -export async function detectUrlMime(url: string) { +export async function detectUrlMime(url: string): Promise { const [path, cleanup] = await createTemp(); try { diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts index e855ac28e..2b8966810 100644 --- a/packages/backend/src/misc/fetch-meta.ts +++ b/packages/backend/src/misc/fetch-meta.ts @@ -1,44 +1,58 @@ +import push from 'web-push'; import { db } from '@/db/postgre.js'; import { Meta } from '@/models/entities/meta.js'; +import { getFetchInstanceMetadataLock } from '@/misc/app-lock.js'; let cache: Meta; -export async function fetchMeta(noCache = false): Promise { - if (!noCache && cache) return cache; +/** + * Performs the primitive database operation to set the server configuration + */ +export async function setMeta(meta: Meta): Promise { + const unlock = await getFetchInstanceMetadataLock('localhost'); - return await db.transaction(async transactionalEntityManager => { - // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する - const metas = await transactionalEntityManager.find(Meta, { + // try to mitigate older bugs where multiple meta entries may have been created + await db.manager.clear(Meta); + await db.manager.insert(Meta, meta); + + cache = meta; + + unlock(); +} + +/** + * Performs the primitive database operation to fetch server configuration. + * If there is no entry yet, inserts a new one. + * Writes to `cache` instead of returning. + */ +async function getMeta(): Promise { + const unlock = await getFetchInstanceMetadataLock('localhost'); + + // new IDs are prioritised because multiple records may have been created due to past bugs + let metas = await db.manager.find(Meta, { + order: { + id: 'DESC', + }, + }); + if (metas.length === 0) { + await db.manager.insert(Meta, { + id: 'x', + }); + metas = await db.manager.find(Meta, { order: { id: 'DESC', }, }); + } + cache = metas[0]; - const meta = metas[0]; - - if (meta) { - cache = meta; - return meta; - } else { - // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う - const saved = await transactionalEntityManager - .upsert( - Meta, - { - id: 'x', - }, - ['id'], - ) - .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); - - cache = saved; - return saved; - } - }); + unlock(); } -setInterval(() => { - fetchMeta(true).then(meta => { - cache = meta; - }); -}, 1000 * 10); +export async function fetchMeta(noCache = false): Promise { + if (!noCache && cache) return cache; + + await getMeta(); + + return cache; +} diff --git a/packages/backend/src/misc/keypair-store.ts b/packages/backend/src/misc/keypair-store.ts index 9babf3ec5..910f96258 100644 --- a/packages/backend/src/misc/keypair-store.ts +++ b/packages/backend/src/misc/keypair-store.ts @@ -3,8 +3,11 @@ import { User } from '@/models/entities/user.js'; import { UserKeypair } from '@/models/entities/user-keypair.js'; import { Cache } from './cache.js'; -const cache = new Cache(Infinity); +const cache = new Cache( + Infinity, + (userId) => UserKeypairs.findOneByOrFail({ userId }), +); export async function getUserKeypair(userId: User['id']): Promise { - return await cache.fetch(userId, () => UserKeypairs.findOneByOrFail({ userId })); + return await cache.fetch(userId); } diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts index c77fe31bf..f0a168369 100644 --- a/packages/backend/src/misc/populate-emojis.ts +++ b/packages/backend/src/misc/populate-emojis.ts @@ -4,14 +4,27 @@ import { Emojis } from '@/models/index.js'; import { Emoji } from '@/models/entities/emoji.js'; import { Note } from '@/models/entities/note.js'; import { query } from '@/prelude/url.js'; +import { HOUR } from '@/const.js'; import { Cache } from './cache.js'; import { isSelfHost, toPunyNullable } from './convert-host.js'; import { decodeReaction } from './reaction-lib.js'; -const cache = new Cache(1000 * 60 * 60 * 12); +/** + * composite cache key: `${host ?? ''}:${name}` + */ +const cache = new Cache( + 12 * HOUR, + async (key) => { + const [host, name] = key.split(':'); + return (await Emojis.findOneBy({ + name, + host: host || IsNull(), + })) || null; + }, +); /** - * 添付用絵文字情報 + * Information needed to attach in ActivityPub */ type PopulatedEmoji = { name: string; @@ -36,28 +49,22 @@ function parseEmojiStr(emojiName: string, noteUserHost: string | null) { const name = match[1]; - // ホスト正規化 const host = toPunyNullable(normalizeHost(match[2], noteUserHost)); return { name, host }; } /** - * 添付用絵文字情報を解決する - * @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能)) - * @param noteUserHost ノートやユーザープロフィールの所有者のホスト - * @returns 絵文字情報, nullは未マッチを意味する + * Resolve emoji information from ActivityPub attachment. + * @param emojiName custom emoji names attached to notes, user profiles or in rections. Colons should not be included. Localhost is denote by @. (see also `decodeReaction`) + * @param noteUserHost host that the content is from, to default to + * @returns emoji information. `null` means not found. */ export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise { const { name, host } = parseEmojiStr(emojiName, noteUserHost); if (name == null) return null; - const queryOrNull = async () => (await Emojis.findOneBy({ - name, - host: host ?? IsNull(), - })) || null; - - const emoji = await cache.fetch(`${name} ${host}`, queryOrNull); + const emoji = await cache.fetch(`${host ?? ''}:${name}`); if (emoji == null) return null; @@ -72,7 +79,7 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu } /** - * 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される) + * Retrieve list of emojis from the cache. Uncached emoji are dropped. */ export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise { const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost))); @@ -103,11 +110,20 @@ export function aggregateNoteEmojis(notes: Note[]) { } /** - * 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します + * Query list of emojis in bulk and add them to the cache. */ export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise { - const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null); + const notCachedEmojis = emojis.filter(emoji => { + // check if the cache has this emoji + return cache.get(`${emoji.host ?? ''}:${emoji.name}`) == null; + }); + + // check if there even are any uncached emoji to handle + if (notCachedEmojis.length === 0) return; + + // query all uncached emoji const emojisQuery: any[] = []; + // group by hosts to try to reduce query size const hosts = new Set(notCachedEmojis.map(e => e.host)); for (const host of hosts) { emojisQuery.push({ @@ -115,11 +131,14 @@ export async function prefetchEmojis(emojis: { name: string; host: string | null host: host ?? IsNull(), }); } - const _emojis = emojisQuery.length > 0 ? await Emojis.find({ + + await Emojis.find({ where: emojisQuery, select: ['name', 'host', 'originalUrl', 'publicUrl'], - }) : []; - for (const emoji of _emojis) { - cache.set(`${emoji.name} ${emoji.host}`, emoji); - } + }).then(emojis => { + // store all emojis into the cache + emojis.forEach(emoji => { + cache.set(`${emoji.host ?? ''}:${emoji.name}`, emoji); + }); + }); } diff --git a/packages/backend/src/misc/renote.ts b/packages/backend/src/misc/renote.ts index 015c26d65..758dcdd05 100644 --- a/packages/backend/src/misc/renote.ts +++ b/packages/backend/src/misc/renote.ts @@ -1,5 +1,5 @@ import { Note } from '@/models/entities/note.js'; -export function isPureRenote(note: Note): boolean { +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/skipped-instances.ts b/packages/backend/src/misc/skipped-instances.ts new file mode 100644 index 000000000..3f13a8f7a --- /dev/null +++ b/packages/backend/src/misc/skipped-instances.ts @@ -0,0 +1,53 @@ +import { Brackets } from 'typeorm'; +import { db } from '@/db/postgre.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; +import { Instances } from '@/models/index.js'; +import { Instance } from '@/models/entities/instance.js'; +import { DAY } from '@/const.js'; + +// Threshold from last contact after which an instance will be considered +// "dead" and should no longer get activities delivered to it. +const deadThreshold = 7 * DAY; + +/** + * Returns the subset of hosts which should be skipped. + * + * @param hosts array of punycoded instance hosts + * @returns array of punycoed instance hosts that should be skipped (subset of hosts parameter) + */ +export async function skippedInstances(hosts: Array): Array { + // first check for blocked instances since that info may already be in memory + const { blockedHosts } = await fetchMeta(); + + const skipped = hosts.filter(host => blockedHosts.includes(host)); + // if possible return early and skip accessing the database + if (skipped.length === hosts.length) return hosts; + + const deadTime = new Date(Date.now() - deadThreshold); + + return skipped.concat( + await db.query( + `SELECT host FROM instance WHERE ("isSuspended" OR "latestStatus" = 410 OR "lastCommunicatedAt" < $1::date) AND host = ANY(string_to_array($2, ','))`, + [ + deadTime.toISOString(), + // don't check hosts again that we already know are suspended + // also avoids adding duplicates to the list + hosts.filter(host => !skipped.includes(host) && !host.includes(',')).join(','), + ], + ) + .then(res => res.map(row => row.host)) + ); +} + +/** + * Returns whether a specific host (punycoded) should be skipped. + * Convenience wrapper around skippedInstances which should only be used if there is a single host to check. + * If you have multiple hosts, consider using skippedInstances instead to do a bulk check. + * + * @param host punycoded instance host + * @returns whether the given host should be skipped + */ +export async function shouldSkipInstance(host: Instance['host']): boolean { + const skipped = await skippedInstances([host]); + return skipped.length > 0; +} diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 9ad7fb83d..95647c1ae 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -62,7 +62,8 @@ export class DriveFile { public size: number; @Column('varchar', { - length: 512, nullable: true, + length: 2048, + nullable: true, comment: 'The comment of the DriveFile.', }) public comment: string | null; diff --git a/packages/backend/src/models/entities/instance.ts b/packages/backend/src/models/entities/instance.ts index 7ea923438..62c541950 100644 --- a/packages/backend/src/models/entities/instance.ts +++ b/packages/backend/src/models/entities/instance.ts @@ -7,7 +7,7 @@ export class Instance { public id: string; /** - * このインスタンスを捕捉した日時 + * Date and time this instance was first seen. */ @Index() @Column('timestamp with time zone', { @@ -16,7 +16,7 @@ export class Instance { public caughtAt: Date; /** - * ホスト + * Hostname */ @Index({ unique: true }) @Column('varchar', { @@ -26,7 +26,7 @@ export class Instance { public host: string; /** - * インスタンスのユーザー数 + * Number of users on this instance. */ @Column('integer', { default: 0, @@ -35,7 +35,7 @@ export class Instance { public usersCount: number; /** - * インスタンスの投稿数 + * Number of notes on this instance. */ @Column('integer', { default: 0, @@ -44,7 +44,7 @@ export class Instance { public notesCount: number; /** - * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数 + * Number of local users who are followed by users from this instance. */ @Column('integer', { default: 0, @@ -52,7 +52,7 @@ export class Instance { public followingCount: number; /** - * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数 + * Number of users from this instance who are followed by local users. */ @Column('integer', { default: 0, @@ -60,7 +60,7 @@ export class Instance { public followersCount: number; /** - * 直近のリクエスト送信日時 + * Timestamp of the latest outgoing HTTP request. */ @Column('timestamp with time zone', { nullable: true, @@ -68,7 +68,7 @@ export class Instance { public latestRequestSentAt: Date | null; /** - * 直近のリクエスト送信時のHTTPステータスコード + * HTTP status code that was received for the last outgoing HTTP request. */ @Column('integer', { nullable: true, @@ -76,7 +76,7 @@ export class Instance { public latestStatus: number | null; /** - * 直近のリクエスト受信日時 + * Timestamp of the latest incoming HTTP request. */ @Column('timestamp with time zone', { nullable: true, @@ -84,13 +84,13 @@ export class Instance { public latestRequestReceivedAt: Date | null; /** - * このインスタンスと最後にやり取りした日時 + * Timestamp of last communication with this instance (incoming or outgoing). */ @Column('timestamp with time zone') public lastCommunicatedAt: Date; /** - * このインスタンスと不通かどうか + * Whether this instance seems unresponsive. */ @Column('boolean', { default: false, @@ -98,7 +98,7 @@ export class Instance { public isNotResponding: boolean; /** - * このインスタンスへの配信を停止するか + * Whether sending activities to this instance has been suspended. */ @Index() @Column('boolean', { diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index df92fb825..dd2598322 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -218,6 +218,11 @@ export class User { }) public token: string | null; + @Column('boolean', { + default: true, + }) + public federateBlocks: boolean; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index b192b6972..a36a575fe 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -6,13 +6,16 @@ import { Packed } from '@/misc/schema.js'; import { awaitAll, Promiseable } from '@/prelude/await-all.js'; import { populateEmojis } from '@/misc/populate-emojis.js'; import { getAntennas } from '@/misc/antenna-cache.js'; -import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; +import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js'; import { Cache } from '@/misc/cache.js'; import { db } from '@/db/postgre.js'; import { Instance } from '../entities/instance.js'; import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, RenoteMutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js'; -const userInstanceCache = new Cache(1000 * 60 * 60 * 3); +const userInstanceCache = new Cache( + 3 * HOUR, + (host) => Instances.findOneBy({ host }).then(x => x ?? undefined), +); type IsUserDetailed = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>; type IsMeAndIsUserDetailed = @@ -27,7 +30,7 @@ const ajv = new Ajv(); const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; const passwordSchema = { type: 'string', minLength: 1 } as const; const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; -const descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const; +const descriptionSchema = { type: 'string', minLength: 1, maxLength: 2048 } as const; const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; @@ -309,17 +312,15 @@ export const UserRepository = db.getRepository(User).extend({ isModerator: user.isModerator || falsy, isBot: user.isBot || falsy, isCat: user.isCat || falsy, - instance: user.host ? userInstanceCache.fetch(user.host, - () => Instances.findOneBy({ host: user.host! }), - v => v != null, - ).then(instance => instance ? { - name: instance.name, - softwareName: instance.softwareName, - softwareVersion: instance.softwareVersion, - iconUrl: instance.iconUrl, - faviconUrl: instance.faviconUrl, - themeColor: instance.themeColor, - } : undefined) : undefined, + instance: !user.host ? undefined : userInstanceCache.fetch(user.host) + .then(instance => !instance ? undefined : { + name: instance.name, + softwareName: instance.softwareName, + softwareVersion: instance.softwareVersion, + iconUrl: instance.iconUrl, + faviconUrl: instance.faviconUrl, + themeColor: instance.themeColor, + }), emojis: populateEmojis(user.emojis, user.host), onlineStatus: this.getOnlineStatus(user), @@ -392,6 +393,7 @@ export const UserRepository = db.getRepository(User).extend({ mutingNotificationTypes: profile!.mutingNotificationTypes, emailNotificationTypes: profile!.emailNotificationTypes, showTimelineReplies: user.showTimelineReplies || falsy, + federateBlocks: user!.federateBlocks, } : {}), ...(opts.includeSecrets ? { diff --git a/packages/backend/src/queue/processors/deliver.ts b/packages/backend/src/queue/processors/deliver.ts index 1f99b305d..638de71dd 100644 --- a/packages/backend/src/queue/processors/deliver.ts +++ b/packages/backend/src/queue/processors/deliver.ts @@ -6,39 +6,20 @@ import Logger from '@/services/logger.js'; import { Instances } from '@/models/index.js'; import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js'; import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; import { toPuny } from '@/misc/convert-host.js'; -import { Cache } from '@/misc/cache.js'; -import { Instance } from '@/models/entities/instance.js'; import { StatusError } from '@/misc/fetch.js'; +import { shouldSkipInstance } from '@/misc/skipped-instances.js'; import { DeliverJobData } from '@/queue/types.js'; -import { LessThan } from 'typeorm'; -import { DAY } from '@/const.js'; const logger = new Logger('deliver'); let latest: string | null = null; -const deadThreshold = 30 * DAY; - export default async (job: Bull.Job) => { const { host } = new URL(job.data.to); const puny = toPuny(host); - // ブロックしてたら中断 - const meta = await fetchMeta(); - if (meta.blockedHosts.includes(puny)) { - return 'skip (blocked)'; - } - - const deadTime = new Date(Date.now() - deadThreshold); - const isSuspendedOrDead = await Instances.countBy([ - { host: puny, isSuspended: true }, - { host: puny, lastCommunicatedAt: LessThan(deadTime) }, - ]); - if (isSuspendedOrDead) { - return 'skip (suspended or dead)'; - } + if (await shouldSkipInstance(puny)) return 'skip'; try { if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) { @@ -81,8 +62,8 @@ export default async (job: Bull.Job) => { if (res instanceof StatusError) { // 4xx if (res.isClientError) { - // HTTPステータスコード4xxはクライアントエラーであり、それはつまり - // 何回再送しても成功することはないということなのでエラーにはしないでおく + // A client error means that something is wrong with the request we are making, + // which means that retrying it makes no sense. return `${res.statusCode} ${res.statusMessage}`; } diff --git a/packages/backend/src/queue/processors/system/check-expired.ts b/packages/backend/src/queue/processors/system/check-expired.ts index 7078d3ccb..5608dc43c 100644 --- a/packages/backend/src/queue/processors/system/check-expired.ts +++ b/packages/backend/src/queue/processors/system/check-expired.ts @@ -1,6 +1,6 @@ import Bull from 'bull'; import { In, LessThan } from 'typeorm'; -import { AttestationChallenges, Mutings, Signins } from '@/models/index.js'; +import { AttestationChallenges, Mutings, PasswordResetRequests, Signins } from '@/models/index.js'; import { publishUserEvent } from '@/services/stream.js'; import { MINUTE, DAY } from '@/const.js'; import { queueLogger } from '@/queue/logger.js'; @@ -35,6 +35,11 @@ export async function checkExpired(job: Bull.Job>, done: createdAt: LessThan(new Date(new Date().getTime() - 5 * MINUTE)), }); + await PasswordResetRequests.delete({ + // this timing should be the same as in @/server/api/endpoints/reset-password.ts + createdAt: LessThan(new Date(new Date().getTime() - 30 * MINUTE)), + }); + logger.succ('Deleted expired mutes, signins and attestation challenges.'); done(); diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index 24e361875..097747970 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -10,8 +10,14 @@ import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; import { IObject, getApId } from './type.js'; import { resolvePerson } from './models/person.js'; -const publicKeyCache = new Cache(Infinity); -const publicKeyByUserIdCache = new Cache(Infinity); +const publicKeyCache = new Cache( + Infinity, + (keyId) => UserPublickeys.findOneBy({ keyId }).then(x => x ?? undefined), +); +const publicKeyByUserIdCache = new Cache( + Infinity, + (userId) => UserPublickeys.findOneBy({ userId }).then(x => x ?? undefined), +); export type UriParseResult = { /** wether the URI was generated by us */ @@ -99,13 +105,9 @@ export default class DbResolver { if (parsed.local) { if (parsed.type !== 'users') return null; - return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({ - id: parsed.id, - }).then(x => x ?? undefined)) ?? null; + return await userByIdCache.fetch(parsed.id) ?? null; } else { - return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({ - uri: parsed.uri, - })); + return await uriPersonCache.fetch(parsed.uri) ?? null; } } @@ -116,20 +118,12 @@ export default class DbResolver { user: CacheableRemoteUser; key: UserPublickey; } | null> { - const key = await publicKeyCache.fetch(keyId, async () => { - const key = await UserPublickeys.findOneBy({ - keyId, - }); - - if (key == null) return null; - - return key; - }, key => key != null); + const key = await publicKeyCache.fetch(keyId); if (key == null) return null; return { - user: await userByIdCache.fetch(key.userId, () => Users.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser, + user: await userByIdCache.fetch(key.userId) as CacheableRemoteUser, key, }; } @@ -145,7 +139,7 @@ export default class DbResolver { if (user == null) return null; - const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOneBy({ userId: user.id }), v => v != null); + const key = await publicKeyByUserIdCache.fetch(user.id); return { user, diff --git a/packages/backend/src/remote/activitypub/deliver-manager.ts b/packages/backend/src/remote/activitypub/deliver-manager.ts index 1f5702ae7..c706968df 100644 --- a/packages/backend/src/remote/activitypub/deliver-manager.ts +++ b/packages/backend/src/remote/activitypub/deliver-manager.ts @@ -2,6 +2,7 @@ import { IsNull, Not } from 'typeorm'; import { ILocalUser, IRemoteUser, User } from '@/models/entities/user.js'; import { Users, Followings } from '@/models/index.js'; import { deliver } from '@/queue/index.js'; +import { skippedInstances } from '@/misc/skipped-instances.js'; //#region types interface IRecipe { @@ -118,26 +119,18 @@ export default class DeliverManager { if (this.recipes.some(r => isFollowers(r))) { // followers deliver - // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう - // ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう? - const followers = await Followings.find({ - where: { - followeeId: this.actor.id, - followerHost: Not(IsNull()), - }, - select: { - followerSharedInbox: true, - followerInbox: true, - }, - }) as { - followerSharedInbox: string | null; - followerInbox: string; - }[]; + const followers = await Followings.createQueryBuilder('followings') + // return either the shared inbox (if available) or the individual inbox + .select('COALESCE(followings.followerSharedInbox, followings.followerInbox)', 'inbox') + // so we don't have to make our inboxes Set work as hard + .distinct(true) + // ...for the specific actors followers + .where('followings.followeeId = :actorId', { actorId: this.actor.id }) + // don't deliver to ourselves + .andWhere('followings.followerHost IS NOT NULL') + .getRawMany(); - for (const following of followers) { - const inbox = following.followerSharedInbox || following.followerInbox; - inboxes.add(inbox); - } + followers.forEach(({ inbox }) => inboxes.add(inbox)); } this.recipes.filter((recipe): recipe is IDirectRecipe => @@ -150,8 +143,19 @@ export default class DeliverManager { ) .forEach(recipe => inboxes.add(recipe.to.inbox!)); + const instancesToSkip = await skippedInstances( + // get (unique) list of hosts + Array.from(new Set( + Array.from(inboxes) + .map(inbox => new URL(inbox).host) + )) + ); + // deliver for (const inbox of inboxes) { + // skip instances as indicated + if (instancesToSkip.includes(new URL(inbox).host)) continue; + deliver(this.actor, this.activity, inbox); } } diff --git a/packages/backend/src/remote/activitypub/kernel/like.ts b/packages/backend/src/remote/activitypub/kernel/like.ts index b9faa38d1..76272eea7 100644 --- a/packages/backend/src/remote/activitypub/kernel/like.ts +++ b/packages/backend/src/remote/activitypub/kernel/like.ts @@ -1,5 +1,5 @@ import { CacheableRemoteUser } from '@/models/entities/user.js'; -import create from '@/services/note/reaction/create.js'; +import { createReaction } from '@/services/note/reaction/create.js'; import { ILike, getApId } from '../type.js'; import { fetchNote, extractEmojis } from '../models/note.js'; @@ -11,7 +11,7 @@ export default async (actor: CacheableRemoteUser, activity: ILike) => { await extractEmojis(activity.tag || [], actor.host).catch(() => null); - return await create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => { + return await createReaction(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => { if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { return 'skip: already reacted'; } else { diff --git a/packages/backend/src/remote/activitypub/kernel/undo/like.ts b/packages/backend/src/remote/activitypub/kernel/undo/like.ts index a7f7e6bc6..6c7b8d18b 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/like.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/like.ts @@ -1,5 +1,5 @@ import { CacheableRemoteUser } from '@/models/entities/user.js'; -import deleteReaction from '@/services/note/reaction/delete.js'; +import { deleteReaction } from '@/services/note/reaction/delete.js'; import { ILike, getApId } from '@/remote/activitypub/type.js'; import { fetchNote } from '@/remote/activitypub/models/note.js'; diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index e5f8344ff..55989206e 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -4,7 +4,7 @@ import config from '@/config/index.js'; import post from '@/services/note/create.js'; import { CacheableRemoteUser } from '@/models/entities/user.js'; import { unique, toArray, toSingle } from '@/prelude/array.js'; -import vote from '@/services/note/polls/vote.js'; +import { vote } from '@/services/note/polls/vote.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; import { extractDbHost, toPuny } from '@/misc/convert-host.js'; diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index 2cc2a146c..99c2b8d16 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -34,7 +34,7 @@ export default async (user: { id: User['id'] }, url: string, object: any) => { * @param user http-signature user * @param url URL to fetch */ -export async function signedGet(url: string, user: { id: User['id'] }) { +export async function signedGet(url: string, user: { id: User['id'] }): Promise { const keypair = await getUserKeypair(user.id); const req = createSignedGet({ diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index f1a8f4914..2dc4ea1e7 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -23,9 +23,7 @@ import Featured from './activitypub/featured.js'; // Init router const router = new Router(); -//#region Routing - -function inbox(ctx: Router.RouterContext) { +function inbox(ctx: Router.RouterContext): void { let signature; try { @@ -43,13 +41,15 @@ function inbox(ctx: Router.RouterContext) { const ACTIVITY_JSON = 'application/activity+json; charset=utf-8'; const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'; -function isActivityPubReq(ctx: Router.RouterContext) { +function isActivityPubReq(ctx: Router.RouterContext): boolean { ctx.response.vary('Accept'); + // if no accept header is supplied, koa returns the 1st, so html is used as a dummy + // i.e. activitypub requests must be explicit const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON); return typeof accepted === 'string' && !accepted.match(/html/); } -export function setResponseType(ctx: Router.RouterContext) { +export function setResponseType(ctx: Router.RouterContext): void { const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON); if (accept === LD_JSON) { ctx.response.type = LD_JSON; @@ -77,7 +77,7 @@ router.get('/notes/:note', async (ctx, next) => { return; } - // リモートだったらリダイレクト + // redirect if remote if (note.userHost != null) { if (note.uri == null || isSelfHost(note.userHost)) { ctx.status = 500; @@ -94,6 +94,15 @@ router.get('/notes/:note', async (ctx, next) => { // note activity router.get('/notes/:note/activity', async ctx => { + if (!isActivityPubReq(ctx)) { + /* + Redirect to the human readable page. in this case using next is not possible, + since there is no human readable page explicitly for the activity. + */ + ctx.redirect(`/notes/${ctx.params.note}`); + return; + } + const note = await Notes.findOneBy({ id: ctx.params.note, userHost: IsNull(), @@ -149,7 +158,7 @@ router.get('/users/:user/publickey', async ctx => { }); // user -async function userInfo(ctx: Router.RouterContext, user: User | null) { +async function userInfo(ctx: Router.RouterContext, user: User | null): Promise { if (user == null) { ctx.status = 404; return; @@ -185,7 +194,6 @@ router.get('/@:user', async (ctx, next) => { await userInfo(ctx, user); }); -//#endregion // emoji router.get('/emojis/:emoji', async ctx => { @@ -206,6 +214,13 @@ router.get('/emojis/:emoji', async ctx => { // like router.get('/likes/:like', async ctx => { + const reaction = await NoteReactions.findOneBy({ id: ctx.params.like }); + + if (reaction == null) { + ctx.status = 404; + return; + } + const note = await Notes.findOneBy({ id: reaction.noteId, visibility: In(['public' as const, 'home' as const]), @@ -216,13 +231,6 @@ router.get('/likes/:like', async ctx => { return; } - const reaction = await NoteReactions.findOneBy({ id: ctx.params.like }); - - if (reaction == null) { - ctx.status = 404; - return; - } - ctx.body = renderActivity(await renderLike(reaction, note)); ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts index 956096367..30a8dbcf5 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -5,59 +5,51 @@ import authenticate, { AuthenticationError } from './authenticate.js'; import call from './call.js'; import { ApiError } from './error.js'; -export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => { +export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise { const body = ctx.is('multipart/form-data') ? (ctx.request as any).body : ctx.method === 'GET' ? ctx.query : ctx.request.body; - const reply = (x?: any, y?: ApiError) => { - if (x == null) { - ctx.status = 204; - } else if (typeof x === 'number' && y) { - ctx.status = x; - ctx.body = { - error: { - message: y!.message, - code: y!.code, - id: y!.id, - kind: y!.kind, - ...(y!.info ? { info: y!.info } : {}), - }, - }; - } else { - // 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない - ctx.body = typeof x === 'string' ? JSON.stringify(x) : x; + const error = (e: ApiError): void => { + ctx.status = e.httpStatusCode; + if (e.httpStatusCode === 401) { + ctx.response.set('WWW-Authenticate', 'Bearer'); } - res(); + ctx.body = { + error: { + message: e!.message, + code: e!.code, + ...(e!.info ? { info: e!.info } : {}), + endpoint: endpoint.name, + }, + }; }; // Authentication // for GET requests, do not even pass on the body parameter as it is considered unsafe - authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(([user, app]) => { + await authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(async ([user, app]) => { // API invoking - call(endpoint.name, user, app, body, ctx).then((res: any) => { + await call(endpoint.name, user, app, body, ctx).then((res: any) => { if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) { ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`); } - reply(res); + if (res == null) { + ctx.status = 204; + } else { + ctx.status = 200; + // If a string is returned, it must be passed through JSON.stringify to be recognized as JSON. + ctx.body = typeof res === 'string' ? JSON.stringify(res) : res; + } }).catch((e: ApiError) => { - reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); + error(e); }); }).catch(e => { if (e instanceof AuthenticationError) { - ctx.response.status = 403; - ctx.response.set('WWW-Authenticate', 'Bearer'); - ctx.response.body = { - message: 'Authentication failed: ' + e.message, - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - kind: 'client', - }; - res(); + error(new ApiError('AUTHENTICATION_FAILED', e.message)); } else { - reply(500, new ApiError()); + error(new ApiError()); } }); -}); +} diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts index 1dc971d2b..f6d6a646a 100644 --- a/packages/backend/src/server/api/authenticate.ts +++ b/packages/backend/src/server/api/authenticate.ts @@ -3,10 +3,13 @@ import { Users, AccessTokens, Apps } from '@/models/index.js'; import { AccessToken } from '@/models/entities/access-token.js'; import { Cache } from '@/misc/cache.js'; import { App } from '@/models/entities/app.js'; -import { localUserByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js'; +import { userByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js'; import isNativeToken from './common/is-native-token.js'; -const appCache = new Cache(Infinity); +const appCache = new Cache( + Infinity, + (id) => Apps.findOneByOrFail({ id }), +); export class AuthenticationError extends Error { constructor(message: string) { @@ -15,8 +18,8 @@ export class AuthenticationError extends Error { } } -export default async (authorization: string | null | undefined, bodyToken: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => { - let token: string | null = null; +export default async (authorization: string | null | undefined, bodyToken: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => { + let maybeToken: string | null = null; // check if there is an authorization header set if (authorization != null) { @@ -27,19 +30,19 @@ export default async (authorization: string | null | undefined, bodyToken: strin // check if OAuth 2.0 Bearer tokens are being used // Authorization schemes are case insensitive if (authorization.substring(0, 7).toLowerCase() === 'bearer ') { - token = authorization.substring(7); + maybeToken = authorization.substring(7); } else { throw new AuthenticationError('unsupported authentication scheme'); } } else if (bodyToken != null) { - token = bodyToken; + maybeToken = bodyToken; } else { return [null, null]; } + const token: string = maybeToken; if (isNativeToken(token)) { - const user = await localUserByNativeTokenCache.fetch(token, - () => Users.findOneBy({ token }) as Promise); + const user = await localUserByNativeTokenCache.fetch(token); if (user == null) { throw new AuthenticationError('unknown token'); @@ -63,14 +66,13 @@ export default async (authorization: string | null | undefined, bodyToken: strin lastUsedAt: new Date(), }); - const user = await localUserByIdCache.fetch(accessToken.userId, - () => Users.findOneBy({ - id: accessToken.userId, - }) as Promise); + const user = await userByIdCache.fetch(accessToken.userId); + + // can't authorize remote users + if (!Users.isLocalUser(user)) return [null, null]; if (accessToken.appId) { - const app = await appCache.fetch(accessToken.appId, - () => Apps.findOneByOrFail({ id: accessToken.appId! })); + const app = await appCache.fetch(accessToken.appId); return [user, { id: accessToken.id, diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index fe1698dbc..89493ce96 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -8,29 +8,16 @@ import endpoints, { IEndpointMeta } from './endpoints.js'; import { ApiError } from './error.js'; import { apiLogger } from './logger.js'; -const accessDenied = { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', -}; - export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { const isSecure = user != null && token == null; const isModerator = user != null && (user.isModerator || user.isAdmin); const ep = endpoints.find(e => e.name === endpoint); - if (ep == null) { - throw new ApiError({ - message: 'No such endpoint.', - code: 'NO_SUCH_ENDPOINT', - id: 'f8080b67-5f9c-4eb7-8c18-7f1eeae8f709', - httpStatusCode: 404, - }); - } + if (ep == null) throw new ApiError('NO_SUCH_ENDPOINT'); if (ep.meta.secure && !isSecure) { - throw new ApiError(accessDenied); + throw new ApiError('ACCESS_DENIED', 'This operation can only be performed with a native token.'); } if (ep.meta.limit && !isModerator) { @@ -49,48 +36,29 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi } // Rate limit - await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor).catch(e => { - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, - }); + await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor).catch(() => { + throw new ApiError('RATE_LIMIT_EXCEEDED'); }); } if (ep.meta.requireCredential && user == null) { - throw new ApiError({ - message: 'Credential required.', - code: 'CREDENTIAL_REQUIRED', - id: '1384574d-a912-4b81-8601-c7b1c4085df1', - httpStatusCode: 401, - }); + throw new ApiError('AUTHENTICATION_REQUIRED'); } if (ep.meta.requireCredential && user!.isSuspended) { - throw new ApiError({ - message: 'Your account has been suspended.', - code: 'YOUR_ACCOUNT_SUSPENDED', - id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370', - httpStatusCode: 403, - }); + throw new ApiError('SUSPENDED'); } if (ep.meta.requireAdmin && !user!.isAdmin) { - throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); + throw new ApiError('ACCESS_DENIED', 'This operation requires administrator privileges.'); } if (ep.meta.requireModerator && !isModerator) { - throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); + throw new ApiError('ACCESS_DENIED', 'This operation requires moderator privileges.'); } if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { - throw new ApiError({ - message: 'Your app does not have the necessary permissions to use this endpoint.', - code: 'PERMISSION_DENIED', - id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', - }); + throw new ApiError('ACCESS_DENIED', 'This operation requires privileges which this token does not grant.'); } // Cast non JSON input @@ -101,11 +69,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi try { data[k] = JSON.parse(data[k]); } catch (e) { - throw new ApiError({ - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '0b5f1631-7c1a-41a6-b399-cce335f34d85', - }, { + throw new ApiError('INVALID_PARAM', { param: k, reason: `cannot cast to ${param.type}`, }); @@ -129,7 +93,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi stack: e.stack, }, }); - throw new ApiError(null, { + throw new ApiError('INTERNAL_ERROR', { e: { message: e.message, code: e.name, diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts index 3ac4ac0ba..150d27218 100644 --- a/packages/backend/src/server/api/common/signup.ts +++ b/packages/backend/src/server/api/common/signup.ts @@ -24,25 +24,13 @@ export async function signup(opts: { // Validate username if (!Users.validateLocalUsername(username)) { - throw new ApiError({ - message: 'This username is invalid.', - code: 'INVALID_USERNAME', - id: 'ece89f3c-d845-4d9a-850b-1735285e8cd4', - kind: 'client', - httpStatusCode: 400, - }); + throw new ApiError('INVALID_USERNAME'); } if (password != null && passwordHash == null) { // Validate password if (!Users.validatePassword(password)) { - throw new ApiError({ - message: 'This password is invalid.', - code: 'INVALID_PASSWORD', - id: 'a941905b-fe7b-43e2-8ecd-50ad3a2287ab', - kind: 'client', - httpStatusCode: 400, - }); + throw new ApiError('INVALID_PASSWORD'); } // Generate hash of password @@ -53,22 +41,14 @@ export async function signup(opts: { // Generate secret const secret = generateUserToken(); - const duplicateUsernameError = { - message: 'This username is not available.', - code: 'USED_USERNAME', - id: '7ddd595e-6860-4593-93c5-9fdbcb80cd81', - kind: 'client', - httpStatusCode: 409, - }; - // Check username duplication if (await Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { - throw new ApiError(duplicateUsernameError); + throw new ApiError('USED_USERNAME'); } // Check deleted username duplication if (await UsedUsernames.findOneBy({ username: username.toLowerCase() })) { - throw new ApiError(duplicateUsernameError); + throw new ApiError('USED_USERNAME'); } const keyPair = await new Promise((res, rej) => @@ -97,7 +77,7 @@ export async function signup(opts: { host: IsNull(), }); - if (exist) throw new ApiError(duplicateUsernameError); + if (exist) throw new ApiError('USED_USERNAME'); account = await transactionalEntityManager.save(new User({ id: genId(), diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts index 6000357bd..840283957 100644 --- a/packages/backend/src/server/api/define.ts +++ b/packages/backend/src/server/api/define.ts @@ -28,22 +28,16 @@ export default function (meta: T, pa fs.unlink(file.path, () => {}); } - if (meta.requireFile && file == null) return Promise.reject(new ApiError({ - message: 'File required.', - code: 'FILE_REQUIRED', - id: '4267801e-70d1-416a-b011-4ee502885d8b', - })); + if (meta.requireFile && file == null) { + return Promise.reject(new ApiError('FILE_REQUIRED')); + } const valid = validate(params); if (!valid) { if (file) cleanup(); const errors = validate.errors!; - const err = new ApiError({ - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '3d81ceae-475f-4600-b2a8-2bc116157532', - }, { + const err = new ApiError('INVALID_PARAM', { param: errors[0].schemaPath, reason: errors[0].message, }); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index e7563555b..3237935d5 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -1,4 +1,5 @@ import { Schema } from '@/misc/schema.js'; +import { errors } from './error.js'; import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; @@ -270,14 +271,12 @@ import * as ep___serverInfo from './endpoints/server-info.js'; import * as ep___stats from './endpoints/stats.js'; import * as ep___sw_register from './endpoints/sw/register.js'; import * as ep___sw_unregister from './endpoints/sw/unregister.js'; -import * as ep___test from './endpoints/test.js'; import * as ep___username_available from './endpoints/username/available.js'; 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_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.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'; @@ -580,14 +579,12 @@ const eps = [ ['stats', ep___stats], ['sw/register', ep___sw_register], ['sw/unregister', ep___sw_unregister], - ['test', ep___test], ['username/available', ep___username_available], ['users', ep___users], ['users/clips', ep___users_clips], ['users/followers', ep___users_followers], ['users/following', ep___users_following], ['users/gallery/posts', ep___users_gallery_posts], - ['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers], ['users/groups/create', ep___users_groups_create], ['users/groups/delete', ep___users_groups_delete], ['users/groups/invitations/accept', ep___users_groups_invitations_accept], @@ -625,13 +622,7 @@ export interface IEndpointMeta { readonly tags?: ReadonlyArray; - readonly errors?: { - readonly [key: string]: { - readonly message: string; - readonly code: string; - readonly id: string; - }; - }; + readonly errors?: ReadonlyArray; readonly res?: Schema; diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts index 0debf2579..656d4c553 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts @@ -8,13 +8,7 @@ export const meta = { requireCredential: true, requireModerator: true, - errors: { - noSuchAnnouncement: { - message: 'No such announcement.', - code: 'NO_SUCH_ANNOUNCEMENT', - id: 'ecad8040-a276-4e85-bda9-015a708d291e', - }, - }, + errors: ['NO_SUCH_ANNOUNCEMENT'], } as const; export const paramDef = { @@ -29,7 +23,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const announcement = await Announcements.findOneBy({ id: ps.id }); - if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); + if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT'); await Announcements.delete(announcement.id); }); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index af1fa4568..6cc94ce3b 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -8,13 +8,7 @@ export const meta = { requireCredential: true, requireModerator: true, - errors: { - noSuchAnnouncement: { - message: 'No such announcement.', - code: 'NO_SUCH_ANNOUNCEMENT', - id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc', - }, - }, + errors: ['NO_SUCH_ANNOUNCEMENT'], } as const; export const paramDef = { @@ -32,7 +26,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const announcement = await Announcements.findOneBy({ id: ps.id }); - if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); + if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT'); await Announcements.update(announcement.id, { updatedAt: new Date(), diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index ebe378c4f..9f36e6f30 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -8,13 +8,7 @@ export const meta = { requireCredential: true, requireModerator: true, - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'caf3ca38-c6e5-472e-a30c-b05377dcc240', - }, - }, + errors: ['NO_SUCH_FILE'], res: { type: 'object', @@ -180,9 +174,7 @@ export default define(meta, paramDef, async (ps, me) => { }], }); - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (file == null) throw new ApiError('NO_SUCH_FILE'); return file; }); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index ecfed084f..ad5b9896c 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -13,13 +13,7 @@ export const meta = { requireCredential: true, requireModerator: true, - errors: { - noSuchFile: { - message: 'No such file.', - code: 'MO_SUCH_FILE', - id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', - }, - }, + errors: ['NO_SUCH_FILE'], } as const; export const paramDef = { @@ -34,7 +28,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const file = await DriveFiles.findOneBy({ id: ps.fileId }); - if (file == null) throw new ApiError(meta.errors.noSuchFile); + if (file == null) throw new ApiError('NO_SUCH_FILE'); const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index e5bbaefe9..a13a47376 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -13,13 +13,7 @@ export const meta = { requireCredential: true, requireModerator: true, - errors: { - noSuchEmoji: { - message: 'No such emoji.', - code: 'NO_SUCH_EMOJI', - id: 'e2785b66-dca3-4087-9cac-b93c541cc425', - }, - }, + errors: ['NO_SUCH_EMOJI', 'INTERNAL_ERROR'], res: { type: 'object', @@ -46,9 +40,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const emoji = await Emojis.findOneBy({ id: ps.emojiId }); - if (emoji == null) { - throw new ApiError(meta.errors.noSuchEmoji); - } + if (emoji == null) throw new ApiError('NO_SUCH_EMOJI'); let driveFile: DriveFile; @@ -56,7 +48,7 @@ export default define(meta, paramDef, async (ps, me) => { // Create file driveFile = await uploadFromUrl({ url: emoji.originalUrl, user: null, force: true }); } catch (e) { - throw new ApiError(); + throw new ApiError('INTERNAL_ERROR', e); } const copied = await Emojis.insert({ diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index 3cb1402e4..ab80e81bb 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -10,13 +10,7 @@ export const meta = { requireCredential: true, requireModerator: true, - errors: { - noSuchEmoji: { - message: 'No such emoji.', - code: 'NO_SUCH_EMOJI', - id: 'be83669b-773a-44b7-b1f8-e5e5170ac3c2', - }, - }, + errors: ['NO_SUCH_EMOJI'], } as const; export const paramDef = { @@ -31,7 +25,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const emoji = await Emojis.findOneBy({ id: ps.id }); - if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); + if (emoji == null) throw new ApiError('NO_SUCH_EMOJI'); await Emojis.delete(emoji.id); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 70820e8df..e451374d5 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -9,13 +9,7 @@ export const meta = { requireCredential: true, requireModerator: true, - errors: { - noSuchEmoji: { - message: 'No such emoji.', - code: 'NO_SUCH_EMOJI', - id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8', - }, - }, + errors: ['NO_SUCH_EMOJI'], } as const; export const paramDef = { @@ -39,7 +33,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps) => { const emoji = await Emojis.findOneBy({ id: ps.id }); - if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); + if (emoji == null) throw new ApiError('NO_SUCH_EMOJI'); await Emojis.update(emoji.id, { updatedAt: new Date(), diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index 94f757bdc..147b7298c 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -9,13 +9,7 @@ export const meta = { requireCredential: true, requireModerator: true, - errors: { - invalidUrl: { - message: 'Invalid URL', - code: 'INVALID_URL', - id: 'fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c', - }, - }, + errors: ['INVALID_URL'], res: { type: 'object', @@ -58,8 +52,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { try { if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only'); - } catch { - throw new ApiError(meta.errors.invalidUrl); + } catch (e) { + throw new ApiError('INVALID_URL', e); } return await addRelay(ps.inbox); diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 93b261c18..e83252c29 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -1,6 +1,5 @@ -import { Meta } from '@/models/entities/meta.js'; import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { db } from '@/db/postgre.js'; +import { fetchMeta, setMeta } from '@/misc/fetch-meta.js'; import define from '../../define.js'; export const meta = { @@ -375,20 +374,10 @@ export default define(meta, paramDef, async (ps, me) => { set.deeplIsPro = ps.deeplIsPro; } - await db.transaction(async transactionalEntityManager => { - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: 'DESC', - }, - }); - - const meta = metas[0]; - - if (meta) { - await transactionalEntityManager.update(Meta, meta.id, set); - } else { - await transactionalEntityManager.save(Meta, set); - } + const meta = await fetchMeta(); + await setMeta({ + ...meta, + ...set, }); insertModerationLog(me, 'updateMeta'); diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index dca31edb1..5fae03808 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -11,19 +11,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchUserList: { - message: 'No such user list.', - code: 'NO_SUCH_USER_LIST', - id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f', - }, - - noSuchUserGroup: { - message: 'No such user group.', - code: 'NO_SUCH_USER_GROUP', - id: 'aa3c0b9a-8cae-47c0-92ac-202ce5906682', - }, - }, + errors: ['NO_SUCH_USER_LIST', 'NO_SUCH_GROUP'], res: { type: 'object', @@ -71,18 +59,14 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (userList == null) { - throw new ApiError(meta.errors.noSuchUserList); - } + if (userList == null) throw new ApiError('NO_SUCH_USER_LIST'); } else if (ps.src === 'group' && ps.userGroupId) { userGroupJoining = await UserGroupJoinings.findOneBy({ userGroupId: ps.userGroupId, userId: user.id, }); - if (userGroupJoining == null) { - throw new ApiError(meta.errors.noSuchUserGroup); - } + if (userGroupJoining == null) throw new ApiError('NO_SUCH_GROUP'); } const antenna = await Antennas.insert({ diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts index d87f9ba00..c3ad26dda 100644 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts @@ -10,13 +10,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchAntenna: { - message: 'No such antenna.', - code: 'NO_SUCH_ANTENNA', - id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df', - }, - }, + errors: ['NO_SUCH_ANTENNA'], } as const; export const paramDef = { @@ -34,9 +28,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } + if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA'); await Antennas.delete(antenna.id); diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index f86b6c7d8..4dbc824a8 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -1,4 +1,4 @@ -import readNote from '@/services/note/read.js'; +import { readNote } from '@/services/note/read.js'; import { Antennas, Notes, AntennaNotes } from '@/models/index.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; @@ -14,13 +14,7 @@ export const meta = { kind: 'read:account', - errors: { - noSuchAntenna: { - message: 'No such antenna.', - code: 'NO_SUCH_ANTENNA', - id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe', - }, - }, + errors: ['NO_SUCH_ANTENNA'], res: { type: 'array', @@ -53,9 +47,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } + if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA'); const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts index 11e6e95c9..76278cd2c 100644 --- a/packages/backend/src/server/api/endpoints/antennas/show.ts +++ b/packages/backend/src/server/api/endpoints/antennas/show.ts @@ -9,13 +9,7 @@ export const meta = { kind: 'read:account', - errors: { - noSuchAntenna: { - message: 'No such antenna.', - code: 'NO_SUCH_ANTENNA', - id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b', - }, - }, + errors: ['NO_SUCH_ANTENNA'], res: { type: 'object', @@ -40,9 +34,7 @@ export default define(meta, paramDef, async (ps, me) => { userId: me.id, }); - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } + if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA'); return await Antennas.pack(antenna); }); diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index e36696486..1bdafec4c 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -10,25 +10,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchAntenna: { - message: 'No such antenna.', - code: 'NO_SUCH_ANTENNA', - id: '10c673ac-8852-48eb-aa1f-f5b67f069290', - }, - - noSuchUserList: { - message: 'No such user list.', - code: 'NO_SUCH_USER_LIST', - id: '1c6b35c9-943e-48c2-81e4-2844989407f7', - }, - - noSuchUserGroup: { - message: 'No such user group.', - code: 'NO_SUCH_USER_GROUP', - id: '109ed789-b6eb-456e-b8a9-6059d567d385', - }, - }, + errors: ['NO_SUCH_ANTENNA', 'NO_SUCH_USER_LIST', 'NO_SUCH_GROUP'], res: { type: 'object', @@ -74,9 +56,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } + if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA'); let userList; let userGroupJoining; @@ -87,18 +67,14 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (userList == null) { - throw new ApiError(meta.errors.noSuchUserList); - } + if (userList == null) throw new ApiError('NO_SUCH_USER_LIST'); } else if (ps.src === 'group' && ps.userGroupId) { userGroupJoining = await UserGroupJoinings.findOneBy({ userGroupId: ps.userGroupId, userId: user.id, }); - if (userGroupJoining == null) { - throw new ApiError(meta.errors.noSuchUserGroup); - } + if (userGroupJoining == null) throw new ApiError('NO_SUCH_GROUP'); } await Antennas.update(antenna.id, { diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index 6e7804953..d20c36e7c 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -12,9 +12,6 @@ export const meta = { max: 30, }, - errors: { - }, - res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index cbebf0044..a1b18cab1 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -24,13 +24,7 @@ export const meta = { max: 30, }, - errors: { - noSuchObject: { - message: 'No such object.', - code: 'NO_SUCH_OBJECT', - id: 'dc94d745-1262-4e63-a17d-fecaa57efc82', - }, - }, + errors: ['NO_SUCH_OBJECT'], res: { optional: false, nullable: false, @@ -83,7 +77,7 @@ export default define(meta, paramDef, async (ps, me) => { if (object) { return object; } else { - throw new ApiError(meta.errors.noSuchObject); + throw new ApiError('NO_SUCH_OBJECT'); } }); diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index c92e542a1..350f988e2 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -2,6 +2,7 @@ import { Apps } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { unique } from '@/prelude/array.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { kinds } from '@/misc/api-permissions.js'; import define from '../../define.js'; export const meta = { @@ -21,10 +22,14 @@ export const paramDef = { properties: { name: { type: 'string' }, description: { type: 'string' }, - permission: { type: 'array', uniqueItems: true, items: { - type: 'string', - // FIXME: add enum of possible permissions - } }, + permission: { + type: 'array', + uniqueItems: true, + items: { + type: 'string', + enum: kinds, + }, + }, callbackUrl: { type: 'string', nullable: true }, }, required: ['name', 'description', 'permission'], diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts index 3b84fa07b..048848437 100644 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ b/packages/backend/src/server/api/endpoints/app/show.ts @@ -5,13 +5,7 @@ import { ApiError } from '../../error.js'; export const meta = { tags: ['app'], - errors: { - noSuchApp: { - message: 'No such app.', - code: 'NO_SUCH_APP', - id: 'dce83913-2dc6-4093-8a7b-71dbb11718a3', - }, - }, + errors: ['NO_SUCH_APP'], res: { type: 'object', @@ -33,14 +27,12 @@ export default define(meta, paramDef, async (ps, user, token) => { const isSecure = user != null && token == null; // Lookup app - const ap = await Apps.findOneBy({ id: ps.appId }); + const app = await Apps.findOneBy({ id: ps.appId }); - if (ap == null) { - throw new ApiError(meta.errors.noSuchApp); - } + if (app == null) throw new ApiError('NO_SUCH_APP'); - return await Apps.pack(ap, user, { + return await Apps.pack(app, user, { detail: true, - includeSecret: isSecure && (ap.userId === user!.id), + includeSecret: isSecure && (app.userId === user!.id), }); }); diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index dac74afcc..691b4a867 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -12,13 +12,7 @@ export const meta = { secure: true, - errors: { - noSuchSession: { - message: 'No such session.', - code: 'NO_SUCH_SESSION', - id: '9c72d8de-391a-43c1-9d06-08d29efde8df', - }, - }, + errors: ['NO_SUCH_SESSION'], } as const; export const paramDef = { @@ -35,9 +29,7 @@ export default define(meta, paramDef, async (ps, user) => { const session = await AuthSessions .findOneBy({ token: ps.token }); - if (session == null) { - throw new ApiError(meta.errors.noSuchSession); - } + if (session == null) throw new ApiError('NO_SUCH_SESSION'); // Generate access token const accessToken = secureRndstr(32, true); diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts index f40d70c2b..eeb51abc6 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts @@ -26,13 +26,7 @@ export const meta = { }, }, - errors: { - noSuchApp: { - message: 'No such app.', - code: 'NO_SUCH_APP', - id: '92f93e63-428e-4f2f-a5a4-39e1407fe998', - }, - }, + errors: ['NO_SUCH_APP'], } as const; export const paramDef = { @@ -51,7 +45,7 @@ export default define(meta, paramDef, async (ps) => { }); if (app == null) { - throw new ApiError(meta.errors.noSuchApp); + throw new ApiError('NO_SUCH_APP'); } // Generate token diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts index 91ff9c12e..cd30bfcca 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/show.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/show.ts @@ -7,13 +7,7 @@ export const meta = { requireCredential: false, - errors: { - noSuchSession: { - message: 'No such session.', - code: 'NO_SUCH_SESSION', - id: 'bd72c97d-eba7-4adb-a467-f171b8847250', - }, - }, + errors: ['NO_SUCH_SESSION'], res: { type: 'object', @@ -52,9 +46,7 @@ export default define(meta, paramDef, async (ps, user) => { token: ps.token, }); - if (session == null) { - throw new ApiError(meta.errors.noSuchSession); - } + if (session == null) throw new ApiError('NO_SUCH_SESSION'); return await AuthSessions.pack(session, user); }); diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts index 36c915ea4..3a741db44 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts @@ -24,25 +24,7 @@ export const meta = { }, }, - errors: { - noSuchApp: { - message: 'No such app.', - code: 'NO_SUCH_APP', - id: 'fcab192a-2c5a-43b7-8ad8-9b7054d8d40d', - }, - - noSuchSession: { - message: 'No such session.', - code: 'NO_SUCH_SESSION', - id: '5b5a1503-8bc8-4bd0-8054-dc189e8cdcb3', - }, - - pendingSession: { - message: 'This session is not completed yet.', - code: 'PENDING_SESSION', - id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e', - }, - }, + errors: ['NO_SUCH_APP', 'NO_SUCH_SESSION', 'PENDING_SESSION'], } as const; export const paramDef = { @@ -61,9 +43,7 @@ export default define(meta, paramDef, async (ps) => { secret: ps.appSecret, }); - if (app == null) { - throw new ApiError(meta.errors.noSuchApp); - } + if (app == null) throw new ApiError('NO_SUCH_APP'); // Fetch token const session = await AuthSessions.findOneBy({ @@ -71,13 +51,9 @@ export default define(meta, paramDef, async (ps) => { appId: app.id, }); - if (session == null) { - throw new ApiError(meta.errors.noSuchSession); - } + if (session == null) throw new ApiError('NO_SUCH_SESSION'); - if (session.userId == null) { - throw new ApiError(meta.errors.pendingSession); - } + if (session.userId == null) throw new ApiError('PENDING_SESSION'); // Lookup access token const accessToken = await AccessTokens.findOneByOrFail({ diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 14d6e121d..7f45ad522 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -17,25 +17,7 @@ export const meta = { kind: 'write:blocks', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e', - }, - - blockeeIsYourself: { - message: 'Blockee is yourself.', - code: 'BLOCKEE_IS_YOURSELF', - id: '88b19138-f28d-42c0-8499-6a31bbd0fdc6', - }, - - alreadyBlocking: { - message: 'You are already blocking that user.', - code: 'ALREADY_BLOCKING', - id: '787fed64-acb9-464a-82eb-afbd745b9614', - }, - }, + errors: ['NO_SUCH_USER', 'BLOCKEE_IS_YOURSELF', 'ALREADY_BLOCKING'], res: { type: 'object', @@ -57,13 +39,11 @@ export default define(meta, paramDef, async (ps, user) => { const blocker = await Users.findOneByOrFail({ id: user.id }); // 自分自身 - if (user.id === ps.userId) { - throw new ApiError(meta.errors.blockeeIsYourself); - } + if (user.id === ps.userId) throw new ApiError('BLOCKEE_IS_YOURSELF'); // Get blockee const blockee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -73,9 +53,7 @@ export default define(meta, paramDef, async (ps, user) => { blockeeId: blockee.id, }); - if (exist != null) { - throw new ApiError(meta.errors.alreadyBlocking); - } + if (exist != null) throw new ApiError('ALREADY_BLOCKING'); await create(blocker, blockee); diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index 53efc5cca..5e9ca9368 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -17,25 +17,7 @@ export const meta = { kind: 'write:blocks', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '8621d8bf-c358-4303-a066-5ea78610eb3f', - }, - - blockeeIsYourself: { - message: 'Blockee is yourself.', - code: 'BLOCKEE_IS_YOURSELF', - id: '06f6fac6-524b-473c-a354-e97a40ae6eac', - }, - - notBlocking: { - message: 'You are not blocking that user.', - code: 'NOT_BLOCKING', - id: '291b2efa-60c6-45c0-9f6a-045c8f9b02cd', - }, - }, + errors: ['NO_SUCH_USER', 'BLOCKEE_IS_YOURSELF', 'NOT_BLOCKING'], res: { type: 'object', @@ -54,16 +36,14 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const blocker = await Users.findOneByOrFail({ id: user.id }); - // Check if the blockee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.blockeeIsYourself); - } + if (user.id === ps.userId) throw new ApiError('BLOCKEE_IS_YOURSELF'); + + const blocker = await Users.findOneByOrFail({ id: user.id }); // Get blockee const blockee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -73,9 +53,7 @@ export default define(meta, paramDef, async (ps, user) => { blockeeId: blockee.id, }); - if (exist == null) { - throw new ApiError(meta.errors.notBlocking); - } + if (exist == null) throw new ApiError('NOT_BLOCKING'); // Delete blocking await deleteBlocking(blocker, blockee); diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index ac8d63dd8..4962bd3da 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -17,13 +17,7 @@ export const meta = { ref: 'Channel', }, - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050', - }, - }, + errors: ['NO_SUCH_FILE'], } as const; export const paramDef = { @@ -45,9 +39,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (banner == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (banner == null) throw new ApiError('NO_SUCH_FILE'); } const channel = await Channels.insert({ diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index 9107ec4e8..99bf508c8 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -11,13 +11,7 @@ export const meta = { kind: 'write:channels', - errors: { - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: 'c0031718-d573-4e85-928e-10039f1fbb68', - }, - }, + errors: ['NO_SUCH_CHANNEL'], } as const; export const paramDef = { @@ -34,9 +28,7 @@ export default define(meta, paramDef, async (ps, user) => { id: ps.channelId, }); - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } + if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); await ChannelFollowings.insert({ id: genId(), diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts index 0e55e73a0..c6a063746 100644 --- a/packages/backend/src/server/api/endpoints/channels/show.ts +++ b/packages/backend/src/server/api/endpoints/channels/show.ts @@ -13,13 +13,7 @@ export const meta = { ref: 'Channel', }, - errors: { - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: '6f6c314b-7486-4897-8966-c04a66a02923', - }, - }, + errors: ['NO_SUCH_CHANNEL'], } as const; export const paramDef = { @@ -36,9 +30,7 @@ export default define(meta, paramDef, async (ps, me) => { id: ps.channelId, }); - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } + if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); return await Channels.pack(channel, me); }); diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 90db2db37..99e13d5ab 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -19,13 +19,7 @@ export const meta = { }, }, - errors: { - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f', - }, - }, + errors: ['NO_SUCH_CHANNEL'], } as const; export const paramDef = { @@ -47,9 +41,7 @@ export default define(meta, paramDef, async (ps, user) => { id: ps.channelId, }); - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } + if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); //#region Construct query const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts index 262af1720..47e5d6d9b 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts @@ -10,13 +10,7 @@ export const meta = { kind: 'write:channels', - errors: { - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6', - }, - }, + errors: ['NO_SUCH_CHANNEL'], } as const; export const paramDef = { @@ -33,9 +27,7 @@ export default define(meta, paramDef, async (ps, user) => { id: ps.channelId, }); - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } + if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); await ChannelFollowings.delete({ followerId: user.id, diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index 4ca9369d1..33557df12 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -15,25 +15,7 @@ export const meta = { ref: 'Channel', }, - errors: { - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: 'f9c5467f-d492-4c3c-9a8d-a70dacc86512', - }, - - accessDenied: { - message: 'You do not have edit privilege of the channel.', - code: 'ACCESS_DENIED', - id: '1fb7cb09-d46a-4fdf-b8df-057788cce513', - }, - - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b', - }, - }, + errors: ['ACCESS_DENIED', 'NO_SUCH_CHANNEL', 'NO_SUCH_FILE'], } as const; export const paramDef = { @@ -53,13 +35,9 @@ export default define(meta, paramDef, async (ps, me) => { id: ps.channelId, }); - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } + if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); - if (channel.userId !== me.id) { - throw new ApiError(meta.errors.accessDenied); - } + if (channel.userId !== me.id) throw new ApiError('ACCESS_DENIED', 'You are not the owner of this channel.'); // eslint:disable-next-line:no-unnecessary-initializer let banner = undefined; @@ -69,9 +47,7 @@ export default define(meta, paramDef, async (ps, me) => { userId: me.id, }); - if (banner == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (banner == null) throw new ApiError('NO_SUCH_FILE'); } else if (ps.bannerId === null) { banner = null; } diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index 15828303b..13b432d13 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -11,25 +11,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', - }, - - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b', - }, - - alreadyClipped: { - message: 'The note has already been clipped.', - code: 'ALREADY_CLIPPED', - id: '734806c4-542c-463a-9311-15c512803965', - }, - }, + errors: ['ALREADY_CLIPPED', 'NO_SUCH_CLIP', 'NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -48,12 +30,10 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } + if (clip == null) throw new ApiError('NO_SUCH_CLIP'); const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); @@ -62,9 +42,7 @@ export default define(meta, paramDef, async (ps, user) => { clipId: clip.id, }); - if (exist != null) { - throw new ApiError(meta.errors.alreadyClipped); - } + if (exist != null) throw new ApiError('ALREADY_CLIPPED'); await ClipNotes.insert({ id: genId(), diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts index a6c10f546..3fd9ee0b1 100644 --- a/packages/backend/src/server/api/endpoints/clips/delete.ts +++ b/packages/backend/src/server/api/endpoints/clips/delete.ts @@ -9,13 +9,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: '70ca08ba-6865-4630-b6fb-8494759aa754', - }, - }, + errors: ['NO_SUCH_CLIP'], } as const; export const paramDef = { @@ -33,9 +27,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } + if (clip == null) throw new ApiError('NO_SUCH_CLIP'); await Clips.delete(clip.id); }); diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index b8614d6e1..8af046d03 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -13,13 +13,7 @@ export const meta = { kind: 'read:account', - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', - }, - }, + errors: ['NO_SUCH_CLIP'], res: { type: 'array', @@ -49,12 +43,10 @@ export default define(meta, paramDef, async (ps, user) => { id: ps.clipId, }); - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } + if (clip == null) throw new ApiError('NO_SUCH_CLIP'); if (!clip.isPublic && (user == null || (clip.userId !== user.id))) { - throw new ApiError(meta.errors.noSuchClip); + throw new ApiError('NO_SUCH_CLIP'); } const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index a6a5eeca2..e6b60178e 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -10,19 +10,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', - }, - - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'aff017de-190e-434b-893e-33a9ff5049d8', - }, - }, + errors: ['NO_SUCH_CLIP', 'NO_SUCH_NOTE', 'NOT_CLIPPED'], } as const; export const paramDef = { @@ -41,17 +29,17 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } + if (clip == null) throw new ApiError('NO_SUCH_CLIP'); const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw e; }); - await ClipNotes.delete({ + const { affected } = await ClipNotes.delete({ noteId: note.id, clipId: clip.id, }); + + if (affected === 0) throw new ApiError('NOT_CLIPPED'); }); diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts index 88a5c2eeb..942890dfe 100644 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ b/packages/backend/src/server/api/endpoints/clips/show.ts @@ -9,13 +9,7 @@ export const meta = { kind: 'read:account', - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', - }, - }, + errors: ['NO_SUCH_CLIP'], res: { type: 'object', @@ -39,12 +33,10 @@ export default define(meta, paramDef, async (ps, me) => { id: ps.clipId, }); - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } + if (clip == null) throw new ApiError('NO_SUCH_CLIP'); if (!clip.isPublic && (me == null || (clip.userId !== me.id))) { - throw new ApiError(meta.errors.noSuchClip); + throw new ApiError('NO_SUCH_CLIP'); } return await Clips.pack(clip); diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 2878042ba..6b26e2b20 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -9,13 +9,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', - }, - }, + errors: ['NO_SUCH_CLIP'], res: { type: 'object', @@ -43,9 +37,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } + if (clip == null) throw new ApiError('NO_SUCH_CLIP'); await Clips.update(clip.id, { name: ps.name, diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 69718b926..bad04f188 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -21,13 +21,7 @@ export const meta = { }, }, - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'c118ece3-2e4b-4296-99d1-51756e32d232', - }, - }, + errors: ['NO_SUCH_FILE'], } as const; export const paramDef = { @@ -46,9 +40,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (file == null) throw new ApiError('NO_SUCH_FILE'); const notes = await Notes.createQueryBuilder('note') .where(':file = ANY(note.fileIds)', { file: file.id }) diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 123eadfce..8cbac7907 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -28,13 +28,7 @@ export const meta = { ref: 'DriveFile', }, - errors: { - invalidFileName: { - message: 'Invalid file name.', - code: 'INVALID_FILE_NAME', - id: 'f449b209-0c60-4e51-84d5-29486263bfd4', - }, - }, + errors: ['INTERNAL_ERROR', 'INVALID_FILE_NAME'], } as const; export const paramDef = { @@ -60,7 +54,7 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => { } else if (name === 'blob') { name = null; } else if (!DriveFiles.validateFileName(name)) { - throw new ApiError(meta.errors.invalidFileName); + throw new ApiError('INVALID_FILE_NAME'); } } else { name = null; @@ -74,7 +68,7 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => { if (e instanceof Error || typeof e === 'string') { apiLogger.error(e); } - throw new ApiError(); + throw new ApiError('INTERNAL_ERROR'); } finally { cleanup!(); } diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index a3f608c9d..c5a8f94b6 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -13,19 +13,7 @@ export const meta = { description: 'Delete an existing drive file.', - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: '908939ec-e52b-4458-b395-1025195cea58', - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '5eb8d909-2540-4970-90b8-dd6f86088121', - }, - }, + errors: ['ACCESS_DENIED', 'NO_SUCH_FILE'], } as const; export const paramDef = { @@ -40,12 +28,10 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const file = await DriveFiles.findOneBy({ id: ps.fileId }); - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (file == null) throw new ApiError('NO_SUCH_FILE'); if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); + throw new ApiError('ACCESS_DENIED'); } // Delete diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index 92796d552..08ac5ae2d 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -18,19 +18,7 @@ export const meta = { ref: 'DriveFile', }, - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: '067bc436-2718-4795-b0fb-ecbe43949e31', - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '25b73c73-68b1-41d0-bad1-381cfdf6579f', - }, - }, + errors: ['ACCESS_DENIED', 'NO_SUCH_FILE'], } as const; export const paramDef = { @@ -69,12 +57,10 @@ export default define(meta, paramDef, async (ps, user) => { }); } - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (file == null) throw new ApiError('NO_SUCH_FILE'); if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); + throw new ApiError('ACCESS_DENIED'); } return await DriveFiles.pack(file, { diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index dedec03e2..3b5e57461 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -12,31 +12,7 @@ export const meta = { description: 'Update the properties of a drive file.', - errors: { - invalidFileName: { - message: 'Invalid file name.', - code: 'INVALID_FILE_NAME', - id: '395e7156-f9f0-475e-af89-53c3c23080c2', - }, - - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'e7778c7e-3af9-49cd-9690-6dbc3e6c972d', - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '01a53b27-82fc-445b-a0c1-b558465a8ed2', - }, - - noSuchFolder: { - message: 'No such folder.', - code: 'NO_SUCH_FOLDER', - id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73', - }, - }, + errors: ['ACCESS_DENIED', 'INVALID_FILE_NAME', 'NO_SUCH_FILE', 'NO_SUCH_FOLDER'], res: { type: 'object', @@ -61,17 +37,15 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const file = await DriveFiles.findOneBy({ id: ps.fileId }); - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (file == null) throw new ApiError('NO_SUCH_FILE'); if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); + throw new ApiError('ACCESS_DENIED'); } if (ps.name) file.name = ps.name; if (!DriveFiles.validateFileName(file.name)) { - throw new ApiError(meta.errors.invalidFileName); + throw new ApiError('INVALID_FILE_NAME'); } if (ps.comment !== undefined) file.comment = ps.comment; @@ -87,9 +61,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } + if (folder == null) throw new ApiError('NO_SUCH_FOLDER'); file.folderId = folder.id; } diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts index 6cb161a7f..ea4e201ff 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts @@ -11,13 +11,7 @@ export const meta = { kind: 'write:drive', - errors: { - noSuchFolder: { - message: 'No such folder.', - code: 'NO_SUCH_FOLDER', - id: '53326628-a00d-40a6-a3cd-8975105c0f95', - }, - }, + errors: ['NO_SUCH_FOLDER'], res: { type: 'object' as const, @@ -46,9 +40,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (parent == null) { - throw new ApiError(meta.errors.noSuchFolder); - } + if (parent == null) throw new ApiError('NO_SUCH_FOLDER'); } // Create folder diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts index a4176312e..76aeba323 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts @@ -10,19 +10,7 @@ export const meta = { kind: 'write:drive', - errors: { - noSuchFolder: { - message: 'No such folder.', - code: 'NO_SUCH_FOLDER', - id: '1069098f-c281-440f-b085-f9932edbe091', - }, - - hasChildFilesOrFolders: { - message: 'This folder has child files or folders.', - code: 'HAS_CHILD_FILES_OR_FOLDERS', - id: 'b0fc8a17-963c-405d-bfbc-859a487295e1', - }, - }, + errors: ['HAS_CHILD_FILES_OR_FOLDERS', 'NO_SUCH_FOLDER'], } as const; export const paramDef = { @@ -41,9 +29,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } + if (folder == null) throw new ApiError('NO_SUCH_FOLDER'); const [childFoldersCount, childFilesCount] = await Promise.all([ DriveFolders.countBy({ parentId: folder.id }), @@ -51,7 +37,7 @@ export default define(meta, paramDef, async (ps, user) => { ]); if (childFoldersCount !== 0 || childFilesCount !== 0) { - throw new ApiError(meta.errors.hasChildFilesOrFolders); + throw new ApiError('HAS_CHILD_FILES_OR_FOLDERS'); } await DriveFolders.delete(folder.id); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts index afb448214..9c8734227 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts @@ -15,13 +15,7 @@ export const meta = { ref: 'DriveFolder', }, - errors: { - noSuchFolder: { - message: 'No such folder.', - code: 'NO_SUCH_FOLDER', - id: 'd74ab9eb-bb09-4bba-bf24-fb58f761e1e9', - }, - }, + errors: ['NO_SUCH_FOLDER'], } as const; export const paramDef = { @@ -40,9 +34,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } + if (folder == null) throw new ApiError('NO_SUCH_FOLDER'); return await DriveFolders.pack(folder, { detail: true, diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index a87420042..8813456bb 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -10,25 +10,7 @@ export const meta = { kind: 'write:drive', - errors: { - noSuchFolder: { - message: 'No such folder.', - code: 'NO_SUCH_FOLDER', - id: 'f7974dac-2c0d-4a27-926e-23583b28e98e', - }, - - noSuchParentFolder: { - message: 'No such parent folder.', - code: 'NO_SUCH_PARENT_FOLDER', - id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1', - }, - - recursiveNesting: { - message: 'It can not be structured like nesting folders recursively.', - code: 'NO_SUCH_PARENT_FOLDER', - id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1', - }, - }, + errors: ['NO_SUCH_FOLDER', 'NO_SUCH_PARENT_FOLDER', 'RECURSIVE_FOLDER'], res: { type: 'object', @@ -55,15 +37,13 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } + if (folder == null) throw new ApiError('NO_SUCH_FOLDER'); if (ps.name) folder.name = ps.name; if (ps.parentId !== undefined) { if (ps.parentId === folder.id) { - throw new ApiError(meta.errors.recursiveNesting); + throw new ApiError('RECURSIVE_FOLDER'); } else if (ps.parentId === null) { folder.parentId = null; } else { @@ -73,9 +53,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (parent == null) { - throw new ApiError(meta.errors.noSuchParentFolder); - } + if (parent == null) throw new ApiError('NO_SUCH_PARENT_FOLDER'); // Check if the circular reference will occur async function checkCircle(folderId: string): Promise { @@ -95,7 +73,7 @@ export default define(meta, paramDef, async (ps, user) => { if (parent.parentId !== null) { if (await checkCircle(parent.parentId)) { - throw new ApiError(meta.errors.recursiveNesting); + throw new ApiError('RECURSIVE_FOLDER'); } } diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts index ace3309ad..2bfd63ff8 100644 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ b/packages/backend/src/server/api/endpoints/federation/followers.ts @@ -5,7 +5,8 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { tags: ['federation'], - requireCredential: false, + requireCredential: true, + requireAdmin: true, res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index 8bbb678ea..febcb9e90 100644 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ b/packages/backend/src/server/api/endpoints/federation/following.ts @@ -5,7 +5,8 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { tags: ['federation'], - requireCredential: false, + requireCredential: true, + requireAdmin: true, res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 2a517cf13..b0b2280f8 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -5,7 +5,7 @@ import define from '../../define.js'; export const meta = { tags: ['federation'], - requireCredential: false, + requireCredential: true, res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index 4768796a9..22923d5df 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -5,7 +5,7 @@ import define from '../../define.js'; export const meta = { tags: ['federation'], - requireCredential: false, + requireCredential: true, res: { oneOf: [{ diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index 59328b031..f76e813b4 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -6,7 +6,7 @@ import define from '../../define.js'; export const meta = { tags: ['federation'], - requireCredential: false, + requireCredential: true, allowGet: true, cacheSec: 60 * 60, diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts index 15a8dbb5f..b6f395e0e 100644 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ b/packages/backend/src/server/api/endpoints/federation/users.ts @@ -5,7 +5,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { tags: ['federation'], - requireCredential: false, + requireCredential: true, res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 05fa22a9e..7c32e2e4d 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -8,7 +8,7 @@ const rssParser = new Parser(); export const meta = { tags: ['meta'], - requireCredential: false, + requireCredential: true, allowGet: true, cacheSec: 60 * 3, } as const; diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index 1bca40f16..90ec95172 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -18,37 +18,7 @@ export const meta = { kind: 'write:following', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5', - }, - - followeeIsYourself: { - message: 'Followee is yourself.', - code: 'FOLLOWEE_IS_YOURSELF', - id: '26fbe7bb-a331-4857-af17-205b426669a9', - }, - - alreadyFollowing: { - message: 'You are already following that user.', - code: 'ALREADY_FOLLOWING', - id: '35387507-38c7-4cb9-9197-300b93783fa0', - }, - - blocking: { - message: 'You are blocking that user.', - code: 'BLOCKING', - id: '4e2206ec-aa4f-4960-b865-6c23ac38e2d9', - }, - - blocked: { - message: 'You are blocked by that user.', - code: 'BLOCKED', - id: 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0', - }, - }, + errors: ['ALREADY_FOLLOWING', 'BLOCKING', 'BLOCKED', 'FOLLOWEE_IS_YOURSELF', 'NO_SUCH_USER'], res: { type: 'object', @@ -70,13 +40,11 @@ export default define(meta, paramDef, async (ps, user) => { const follower = user; // 自分自身 - if (user.id === ps.userId) { - throw new ApiError(meta.errors.followeeIsYourself); - } + if (user.id === ps.userId) throw new ApiError('FOLLOWEE_IS_YOURSELF'); // Get followee const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -86,16 +54,14 @@ export default define(meta, paramDef, async (ps, user) => { followeeId: followee.id, }); - if (exist != null) { - throw new ApiError(meta.errors.alreadyFollowing); - } + if (exist != null) throw new ApiError('ALREADY_FOLLOWING'); try { await create(follower, followee); } catch (e) { if (e instanceof IdentifiableError) { - if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking); - if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked); + if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError('BLOCKING'); + if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError('BLOCKED'); } throw e; } diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index 3f9c217de..72c1ce6c6 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -17,25 +17,7 @@ export const meta = { kind: 'write:following', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8', - }, - - followeeIsYourself: { - message: 'Followee is yourself.', - code: 'FOLLOWEE_IS_YOURSELF', - id: 'd9e400b9-36b0-4808-b1d8-79e707f1296c', - }, - - notFollowing: { - message: 'You are not following that user.', - code: 'NOT_FOLLOWING', - id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09', - }, - }, + errors: ['FOLLOWEE_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWING'], res: { type: 'object', @@ -57,13 +39,11 @@ export default define(meta, paramDef, async (ps, user) => { const follower = user; // Check if the followee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.followeeIsYourself); - } + if (user.id === ps.userId) throw new ApiError('FOLLOWEE_IS_YOURSELF'); // Get followee const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -73,9 +53,7 @@ export default define(meta, paramDef, async (ps, user) => { followeeId: followee.id, }); - if (exist == null) { - throw new ApiError(meta.errors.notFollowing); - } + if (exist == null) throw new ApiError('NOT_FOLLOWING'); await deleteFollowing(follower, followee); diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index b9e84d81a..437d20ae7 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -17,25 +17,7 @@ export const meta = { kind: 'write:following', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8', - }, - - followerIsYourself: { - message: 'Follower is yourself.', - code: 'FOLLOWER_IS_YOURSELF', - id: '07dc03b9-03da-422d-885b-438313707662', - }, - - notFollowing: { - message: 'The other use is not following you.', - code: 'NOT_FOLLOWING', - id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09', - }, - }, + errors: ['FOLLOWER_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWING'], res: { type: 'object', @@ -57,13 +39,11 @@ export default define(meta, paramDef, async (ps, user) => { const followee = user; // Check if the follower is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.followerIsYourself); - } + if (user.id === ps.userId) throw new ApiError('FOLLOWER_IS_YOURSELF'); // Get follower const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -73,9 +53,7 @@ export default define(meta, paramDef, async (ps, user) => { followeeId: followee.id, }); - if (exist == null) { - throw new ApiError(meta.errors.notFollowing); - } + if (exist == null) throw new ApiError('NOT_FOLLOWING'); await deleteFollowing(follower, followee); diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts index e5df55375..cd30eab98 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts @@ -10,18 +10,7 @@ export const meta = { kind: 'write:following', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '66ce1645-d66c-46bb-8b79-96739af885bd', - }, - noFollowRequest: { - message: 'No follow request.', - code: 'NO_FOLLOW_REQUEST', - id: 'bcde4f8b-0913-4614-8881-614e522fb041', - }, - }, + errors: ['NO_SUCH_USER', 'NO_SUCH_FOLLOW_REQUEST'], } as const; export const paramDef = { @@ -36,12 +25,12 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { // Fetch follower const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); await acceptFollowRequest(user, follower).catch(e => { - if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError(meta.errors.noFollowRequest); + if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError('NO_SUCH_FOLLOW_REQUEST'); throw e; }); diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts index 5f3a9c691..3827007f1 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -12,19 +12,7 @@ export const meta = { kind: 'write:following', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '4e68c551-fc4c-4e46-bb41-7d4a37bf9dab', - }, - - followRequestNotFound: { - message: 'Follow request not found.', - code: 'FOLLOW_REQUEST_NOT_FOUND', - id: '089b125b-d338-482a-9a09-e2622ac9f8d4', - }, - }, + errors: ['NO_SUCH_USER', 'NO_SUCH_FOLLOW_REQUEST'], res: { type: 'object', @@ -45,7 +33,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { // Fetch followee const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -53,7 +41,7 @@ export default define(meta, paramDef, async (ps, user) => { await cancelFollowRequest(followee, user); } catch (e) { if (e instanceof IdentifiableError) { - if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(meta.errors.followRequestNotFound); + if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError('NO_SUCH_FOLLOW_REQUEST'); } throw e; } diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts index cebe60428..8de83d508 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -10,13 +10,7 @@ export const meta = { kind: 'write:following', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'abc2ffa6-25b2-4380-ba99-321ff3a94555', - }, - }, + errors: ['NO_SUCH_USER'], } as const; export const paramDef = { @@ -31,7 +25,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { // Fetch follower const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index 6757dba1f..a22b603a5 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -22,10 +22,6 @@ export const meta = { optional: false, nullable: false, ref: 'GalleryPost', }, - - errors: { - - }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts index 83bae3af8..65c0e62d0 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts @@ -9,13 +9,7 @@ export const meta = { kind: 'write:gallery', - errors: { - noSuchPost: { - message: 'No such post.', - code: 'NO_SUCH_POST', - id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5', - }, - }, + errors: ['NO_SUCH_POST'], } as const; export const paramDef = { @@ -33,9 +27,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } + 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 index d58427a64..3e0eda503 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -10,25 +10,7 @@ export const meta = { kind: 'write:gallery-likes', - errors: { - noSuchPost: { - message: 'No such post.', - code: 'NO_SUCH_POST', - id: '56c06af3-1287-442f-9701-c93f7c4a62ff', - }, - - yourPost: { - message: 'You cannot like your post.', - code: 'YOUR_POST', - id: 'f78f1511-5ebc-4478-a888-1198d752da68', - }, - - alreadyLiked: { - message: 'The post has already been liked.', - code: 'ALREADY_LIKED', - id: '40e9ed56-a59c-473a-bf3f-f289c54fb5a7', - }, - }, + errors: ['NO_SUCH_POST', 'ALREADY_LIKED'], } as const; export const paramDef = { @@ -42,13 +24,7 @@ export const paramDef = { // 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(meta.errors.noSuchPost); - } - - if (post.userId === user.id) { - throw new ApiError(meta.errors.yourPost); - } + if (post == null) throw new ApiError('NO_SUCH_POST'); // if already liked const exist = await GalleryLikes.findOneBy({ @@ -56,9 +32,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (exist != null) { - throw new ApiError(meta.errors.alreadyLiked); - } + if (exist != null) throw new ApiError('ALREADY_LIKED'); // Create like await GalleryLikes.insert({ diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts index 5fa28b48b..640048028 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts @@ -7,13 +7,7 @@ export const meta = { requireCredential: false, - errors: { - noSuchPost: { - message: 'No such post.', - code: 'NO_SUCH_POST', - id: '1137bf14-c5b0-4604-85bb-5b5371b1cd45', - }, - }, + errors: ['NO_SUCH_POST'], res: { type: 'object', @@ -36,9 +30,7 @@ export default define(meta, paramDef, async (ps, me) => { id: ps.postId, }); - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } + 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 index fd1a10f2b..61d71905e 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -9,19 +9,7 @@ export const meta = { kind: 'write:gallery-likes', - errors: { - noSuchPost: { - message: 'No such post.', - code: 'NO_SUCH_POST', - id: 'c32e6dd0-b555-4413-925e-b3757d19ed84', - }, - - notLiked: { - message: 'You have not liked that post.', - code: 'NOT_LIKED', - id: 'e3e8e06e-be37-41f7-a5b4-87a8250288f0', - }, - }, + errors: ['NO_SUCH_POST', 'NOT_LIKED'], } as const; export const paramDef = { @@ -35,18 +23,14 @@ export const paramDef = { // 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(meta.errors.noSuchPost); - } + 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(meta.errors.notLiked); - } + if (exist == null) throw new ApiError('NOT_LIKED'); // Delete like await GalleryLikes.delete(exist.id); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index 94be32b02..20cab9243 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -20,10 +20,6 @@ export const meta = { optional: false, nullable: false, ref: 'GalleryPost', }, - - errors: { - - }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts index ae3d1449f..e66661a68 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts @@ -14,13 +14,7 @@ export const meta = { ref: 'Hashtag', }, - errors: { - noSuchHashtag: { - message: 'No such hashtag.', - code: 'NO_SUCH_HASHTAG', - id: '110ee688-193e-4a3a-9ecf-c167b2e6981e', - }, - }, + errors: ['NO_SUCH_HASHTAG'], } as const; export const paramDef = { @@ -34,9 +28,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const hashtag = await Hashtags.findOneBy({ name: normalizeForSearch(ps.tag) }); - if (hashtag == null) { - throw new ApiError(meta.errors.noSuchHashtag); - } + if (hashtag == null) throw new ApiError('NO_SUCH_HASHTAG'); return await Hashtags.pack(hashtag); }); diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index f3ea6a42f..ed5ae4259 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -13,31 +13,7 @@ export const meta = { max: 1, }, - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'ebb53e5f-6574-9c0c-0b92-7ca6def56d7e', - }, - - unexpectedFileType: { - message: 'We need csv file.', - code: 'UNEXPECTED_FILE_TYPE', - id: 'b6fab7d6-d945-d67c-dfdb-32da1cd12cfe', - }, - - tooBigFile: { - message: 'That file is too big.', - code: 'TOO_BIG_FILE', - id: 'b7fbf0b1-aeef-3b21-29ef-fadd4cb72ccf', - }, - - emptyFile: { - message: 'That file is empty.', - code: 'EMPTY_FILE', - id: '6f3a4dcc-f060-a707-4950-806fbdbe60d6', - }, - }, + errors: ['EMPTY_FILE', 'FILE_TOO_BIG', 'NO_SUCH_FILE'], } as const; export const paramDef = { @@ -52,10 +28,9 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const file = await DriveFiles.findOneBy({ id: ps.fileId }); - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + if (file == null) throw new ApiError('EMPTY_FILE'); + if (file.size > 50000) throw new ApiError('FILE_TOO_BIG'); + if (file.size === 0) throw new ApiError('EMPTY_FILE'); createImportBlockingJob(user, file.id); }); diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index e5a4f1adf..b0d1a2dba 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -12,31 +12,7 @@ export const meta = { max: 1, }, - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'b98644cf-a5ac-4277-a502-0b8054a709a3', - }, - - unexpectedFileType: { - message: 'We need csv file.', - code: 'UNEXPECTED_FILE_TYPE', - id: '660f3599-bce0-4f95-9dde-311fd841c183', - }, - - tooBigFile: { - message: 'That file is too big.', - code: 'TOO_BIG_FILE', - id: 'dee9d4ed-ad07-43ed-8b34-b2856398bc60', - }, - - emptyFile: { - message: 'That file is empty.', - code: 'EMPTY_FILE', - id: '31a1b42c-06f7-42ae-8a38-a661c5c9f691', - }, - }, + errors: ['EMPTY_FILE', 'FILE_TOO_BIG', 'NO_SUCH_FILE'], } as const; export const paramDef = { @@ -51,10 +27,9 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const file = await DriveFiles.findOneBy({ id: ps.fileId }); - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + if (file == null) throw new ApiError('NO_SUCH_FILE'); + if (file.size > 50000) throw new ApiError('FILE_TOO_BIG'); + if (file.size === 0) throw new ApiError('EMPTY_FILE'); createImportFollowingJob(user, file.id); }); diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index 29acb5638..37149408b 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -13,31 +13,7 @@ export const meta = { max: 1, }, - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'e674141e-bd2a-ba85-e616-aefb187c9c2a', - }, - - unexpectedFileType: { - message: 'We need csv file.', - code: 'UNEXPECTED_FILE_TYPE', - id: '568c6e42-c86c-ba09-c004-517f83f9f1a8', - }, - - tooBigFile: { - message: 'That file is too big.', - code: 'TOO_BIG_FILE', - id: '9b4ada6d-d7f7-0472-0713-4f558bd1ec9c', - }, - - emptyFile: { - message: 'That file is empty.', - code: 'EMPTY_FILE', - id: 'd2f12af1-e7b4-feac-86a3-519548f2728e', - }, - }, + errors: ['EMPTY_FILE', 'FILE_TOO_BIG', 'NO_SUCH_FILE'], } as const; export const paramDef = { @@ -52,10 +28,9 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const file = await DriveFiles.findOneBy({ id: ps.fileId }); - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + if (file == null) throw new ApiError('NO_SUCH_FILE'); + if (file.size > 50000) throw new ApiError('FILE_TOO_BIG'); + if (file.size === 0) throw new ApiError('EMPTY_FILE'); createImportMutingJob(user, file.id); }); diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index 1158af3a1..67d6afd0c 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -12,31 +12,7 @@ export const meta = { max: 1, }, - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'ea9cc34f-c415-4bc6-a6fe-28ac40357049', - }, - - unexpectedFileType: { - message: 'We need csv file.', - code: 'UNEXPECTED_FILE_TYPE', - id: 'a3c9edda-dd9b-4596-be6a-150ef813745c', - }, - - tooBigFile: { - message: 'That file is too big.', - code: 'TOO_BIG_FILE', - id: 'ae6e7a22-971b-4b52-b2be-fc0b9b121fe9', - }, - - emptyFile: { - message: 'That file is empty.', - code: 'EMPTY_FILE', - id: '99efe367-ce6e-4d44-93f8-5fae7b040356', - }, - }, + errors: ['EMPTY_FILE', 'FILE_TOO_BIG', 'NO_SUCH_FILE'], } as const; export const paramDef = { @@ -51,10 +27,9 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const file = await DriveFiles.findOneBy({ id: ps.fileId }); - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + if (file == null) throw new ApiError('NO_SUCH_FILE'); + if (file.size > 30000) throw new ApiError('FILE_TOO_BIG'); + if (file.size === 0) throw new ApiError('EMPTY_FILE'); createImportUserListsJob(user, file.id); }); diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 237d76da8..4a2ccdc79 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,7 +1,7 @@ import { Brackets } from 'typeorm'; import { notificationTypes } from 'foundkey-js'; import { Notifications, Followings, Mutings, Users, UserProfiles } from '@/models/index.js'; -import read from '@/services/note/read.js'; +import { readNote } from '@/services/note/read.js'; import { readNotification } from '../../common/read-notification.js'; import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; @@ -137,7 +137,7 @@ export default define(meta, paramDef, async (ps, user) => { const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!); if (notes.length > 0) { - read(user.id, notes); + readNote(user.id, notes); } return await Notifications.packMany(notifications, user.id); diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts index 64f6a719d..e3a58bc70 100644 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ b/packages/backend/src/server/api/endpoints/i/pin.ts @@ -10,25 +10,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '56734f8b-3928-431e-bf80-6ff87df40cb3', - }, - - pinLimitExceeded: { - message: 'You can not pin notes any more.', - code: 'PIN_LIMIT_EXCEEDED', - id: '72dab508-c64d-498f-8740-a8eec1ba385a', - }, - - alreadyPinned: { - message: 'That note has already been pinned.', - code: 'ALREADY_PINNED', - id: '8b18c2b7-68fe-4edb-9892-c0cbaeb6c913', - }, - }, + errors: ['ALREADY_PINNED', 'NO_SUCH_NOTE', 'PIN_LIMIT_EXCEEDED'], res: { type: 'object', @@ -48,9 +30,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { await addPinned(user, ps.noteId).catch(e => { - if (e.id === '70c4e51f-5bea-449c-a030-53bee3cce202') throw new ApiError(meta.errors.noSuchNote); - if (e.id === '15a018eb-58e5-4da1-93be-330fcc5e4e1a') throw new ApiError(meta.errors.pinLimitExceeded); - if (e.id === '23f0cf4e-59a3-4276-a91d-61a5891c1514') throw new ApiError(meta.errors.alreadyPinned); + if (e.id === '70c4e51f-5bea-449c-a030-53bee3cce202') throw new ApiError('NO_SUCH_NOTE'); + if (e.id === '15a018eb-58e5-4da1-93be-330fcc5e4e1a') throw new ApiError('PIN_LIMIT_EXCEEDED'); + if (e.id === '23f0cf4e-59a3-4276-a91d-61a5891c1514') throw new ApiError('ALREADY_PINNED'); throw e; }); diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index 44d533667..6d7e8954c 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -11,13 +11,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchAnnouncement: { - message: 'No such announcement.', - code: 'NO_SUCH_ANNOUNCEMENT', - id: '184663db-df88-4bc2-8b52-fb85f0681939', - }, - }, + errors: ['NO_SUCH_ANNOUNCEMENT'], } as const; export const paramDef = { @@ -33,9 +27,7 @@ export default define(meta, paramDef, async (ps, user) => { // Check if announcement exists const announcement = await Announcements.findOneBy({ id: ps.announcementId }); - if (announcement == null) { - throw new ApiError(meta.errors.noSuchAnnouncement); - } + if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT'); // Check if already read const read = await AnnouncementReads.findOneBy({ @@ -43,9 +35,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (read != null) { - return; - } + if (read != null) return; // Create read await AnnouncementReads.insert({ 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 744b1d05b..7addd0fab 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 @@ -7,13 +7,7 @@ export const meta = { secure: true, - errors: { - noSuchKey: { - message: 'No such key.', - code: 'NO_SUCH_KEY', - id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a', - }, - }, + errors: ['NO_SUCH_KEY'], } as const; export const paramDef = { @@ -37,9 +31,7 @@ export default define(meta, paramDef, async (ps, user) => { const item = await query.getOne(); - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } + if (item == null) throw new ApiError('NO_SUCH_KEY'); return { updatedAt: item.updatedAt, 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 1bae3c886..a9ca60485 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -7,13 +7,7 @@ export const meta = { secure: true, - errors: { - noSuchKey: { - message: 'No such key.', - code: 'NO_SUCH_KEY', - id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', - }, - }, + errors: ['NO_SUCH_KEY'], } as const; export const paramDef = { @@ -37,9 +31,7 @@ export default define(meta, paramDef, async (ps, user) => { const item = await query.getOne(); - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } + if (item == null) throw new ApiError('NO_SUCH_KEY'); return item.value; }); 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 8467b2513..f656f3d83 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -7,13 +7,7 @@ export const meta = { secure: true, - errors: { - noSuchKey: { - message: 'No such key.', - code: 'NO_SUCH_KEY', - id: '1fac4e8a-a6cd-4e39-a4a5-3a7e11f1b019', - }, - }, + errors: ['NO_SUCH_KEY'], } as const; export const paramDef = { @@ -37,9 +31,7 @@ export default define(meta, paramDef, async (ps, user) => { const item = await query.getOne(); - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } + if (item == null) throw new ApiError('NO_SUCH_KEY'); await RegistryItems.remove(item); }); diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts index 2c63f8a88..88cb751b4 100644 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ b/packages/backend/src/server/api/endpoints/i/unpin.ts @@ -10,13 +10,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '454170ce-9d63-4a43-9da1-ea10afe81e21', - }, - }, + errors: ['NO_SUCH_NOTE'], res: { type: 'object', @@ -36,7 +30,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { await removePinned(user, ps.noteId).catch(e => { - if (e.id === 'b302d4cf-c050-400a-bbb3-be208681f40c') throw new ApiError(meta.errors.noSuchNote); + if (e.id === 'b302d4cf-c050-400a-bbb3-be208681f40c') throw new ApiError('NO_SUCH_NOTE'); throw e; }); diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index 3f9411de4..cb3d6356a 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -19,19 +19,9 @@ export const meta = { max: 3, }, - errors: { - incorrectPassword: { - message: 'Incorrect password.', - code: 'INCORRECT_PASSWORD', - id: 'e54c1d7e-e7d6-4103-86b6-0a95069b4ad3', - }, - - unavailable: { - message: 'Unavailable email address.', - code: 'UNAVAILABLE', - id: 'a2defefb-f220-8849-0af6-17f816099323', - }, - }, + // FIXME: refactor to remove both of these errors? + // the password should not be passed as it is not compatible with using OAuth + errors: ['ACCESS_DENIED', 'INTERNAL_ERROR'], } as const; export const paramDef = { @@ -50,15 +40,11 @@ export default define(meta, paramDef, async (ps, user) => { // Compare password const same = await bcrypt.compare(ps.password, profile.password!); - if (!same) { - throw new ApiError(meta.errors.incorrectPassword); - } + if (!same) throw new ApiError('ACCESS_DENIED'); if (ps.email != null) { const available = await validateEmailForAccount(ps.email); - if (!available) { - throw new ApiError(meta.errors.unavailable); - } + if (!available) throw new ApiError('INTERNAL_ERROR'); } await UserProfiles.update(user.id, { diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index c9557eadd..57c349803 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -22,43 +22,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchAvatar: { - message: 'No such avatar file.', - code: 'NO_SUCH_AVATAR', - id: '539f3a45-f215-4f81-a9a8-31293640207f', - }, - - noSuchBanner: { - message: 'No such banner file.', - code: 'NO_SUCH_BANNER', - id: '0d8f5629-f210-41c2-9433-735831a58595', - }, - - avatarNotAnImage: { - message: 'The file specified as an avatar is not an image.', - code: 'AVATAR_NOT_AN_IMAGE', - id: 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191', - }, - - bannerNotAnImage: { - message: 'The file specified as a banner is not an image.', - code: 'BANNER_NOT_AN_IMAGE', - id: '75aedb19-2afd-4e6d-87fc-67941256fa60', - }, - - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: '8e01b590-7eb9-431b-a239-860e086c408e', - }, - - invalidRegexp: { - message: 'Invalid Regular Expression.', - code: 'INVALID_REGEXP', - id: '0d786918-10df-41cd-8f33-8dec7d9a89a5', - }, - }, + errors: ['INVALID_REGEXP', 'NO_SUCH_FILE', 'NO_SUCH_PAGE', 'NOT_AN_IMAGE'], res: { type: 'object', @@ -117,6 +81,7 @@ export const paramDef = { emailNotificationTypes: { type: 'array', items: { type: 'string', } }, + federateBlocks: { type: 'boolean' }, }, } as const; @@ -142,12 +107,12 @@ export default define(meta, paramDef, async (ps, _user, token) => { // validate regular expression syntax ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { const regexp = x.match(/^\/(.+)\/(.*)$/); - if (!regexp) throw new ApiError(meta.errors.invalidRegexp); + if (!regexp) throw new ApiError('INVALID_REGEXP'); try { new RE2(regexp[1], regexp[2]); } catch (err) { - throw new ApiError(meta.errors.invalidRegexp); + throw new ApiError('INVALID_REGEXP'); } }); @@ -165,6 +130,7 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; + if (typeof ps.federateBlocks === 'boolean') updates.federateBlocks = ps.federateBlocks; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; @@ -174,21 +140,21 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (ps.avatarId) { const avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); - if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); - if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage); + if (avatar == null || avatar.userId !== user.id) throw new ApiError('NO_SUCH_FILE', 'Avatar file not found.'); + if (!avatar.type.startsWith('image/')) throw new ApiError('NOT_AN_IMAGE', 'Avatar file is not an image.'); } if (ps.bannerId) { const banner = await DriveFiles.findOneBy({ id: ps.bannerId }); - if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); - if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage); + if (banner == null || banner.userId !== user.id) throw new ApiError('NO_SUCH_FILE', 'Banner file not found.'); + if (!banner.type.startsWith('image/')) throw new ApiError('BANNER_NOT_AN_IMAGE', 'Banner file is not an image.'); } if (ps.pinnedPageId) { const page = await Pages.findOneBy({ id: ps.pinnedPageId }); - if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage); + if (page == null || page.userId !== user.id) throw new ApiError('NO_SUCH_PAGE'); profileUpdates.pinnedPageId = page.id; } else if (ps.pinnedPageId === null) { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts index 81ac30541..61778aa22 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts @@ -10,13 +10,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchWebhook: { - message: 'No such webhook.', - code: 'NO_SUCH_WEBHOOK', - id: 'bae73e5a-5522-4965-ae19-3a8688e71d82', - }, - }, + errors: ['NO_SUCH_WEBHOOK'], } as const; export const paramDef = { @@ -34,9 +28,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); - } + if (webhook == null) throw new ApiError('NO_SUCH_WEBHOOK'); await Webhooks.delete(webhook.id); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index d45a39813..21baed811 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -9,13 +9,7 @@ export const meta = { kind: 'read:account', - errors: { - noSuchWebhook: { - message: 'No such webhook.', - code: 'NO_SUCH_WEBHOOK', - id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098', - }, - }, + errors: ['NO_SUCH_WEBHOOK'], } as const; export const paramDef = { @@ -33,9 +27,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); - } + if (webhook == null) throw new ApiError('NO_SUCH_WEBHOOK'); return webhook; }); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 0ffa8a318..d3076ba16 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -11,14 +11,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchWebhook: { - message: 'No such webhook.', - code: 'NO_SUCH_WEBHOOK', - id: 'fb0fea69-da18-45b1-828d-bd4fd1612518', - }, - }, - + errors: ['NO_SUCH_WEBHOOK'], } as const; export const paramDef = { @@ -43,9 +36,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); - } + if (webhook == null) throw new ApiError('NO_SUCH_WEBHOOK'); await Webhooks.update(webhook.id, { name: ps.name, diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts index c8006f490..88e17a054 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages.ts @@ -23,25 +23,7 @@ export const meta = { }, }, - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '11795c64-40ea-4198-b06e-3c873ed9039d', - }, - - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f', - }, - - groupAccessDenied: { - message: 'You can not read messages of groups that you have not joined.', - code: 'GROUP_ACCESS_DENIED', - id: 'a053a8dd-a491-4718-8f87-50775aad9284', - }, - }, + errors: ['ACCESS_DENIED', 'NO_SUCH_USER', 'NO_SUCH_GROUP'], } as const; export const paramDef = { @@ -73,7 +55,7 @@ export default define(meta, paramDef, async (ps, user) => { if (ps.userId != null) { // Fetch recipient (user) const recipient = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -110,9 +92,7 @@ export default define(meta, paramDef, async (ps, user) => { // Fetch recipient (group) const recipientGroup = await UserGroups.findOneBy({ id: ps.groupId }); - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } + if (recipientGroup == null) throw new ApiError('NO_SUCH_GROUP'); // check joined const joining = await UserGroupJoinings.findOneBy({ @@ -120,9 +100,7 @@ export default define(meta, paramDef, async (ps, user) => { userGroupId: recipientGroup.id, }); - if (joining == null) { - throw new ApiError(meta.errors.groupAccessDenied); - } + if (joining == null) throw new ApiError('ACCESS_DENIED', 'You have to join a group to read messages in it.'); const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) .andWhere('message.groupId = :groupId', { groupId: recipientGroup.id }); diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts index d930a5871..020d596fc 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts @@ -19,69 +19,73 @@ export const meta = { ref: 'MessagingMessage', }, - errors: { - recipientIsYourself: { - message: 'You can not send a message to yourself.', - code: 'RECIPIENT_IS_YOURSELF', - id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '11795c64-40ea-4198-b06e-3c873ed9039d', - }, - - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537', - }, - - groupAccessDenied: { - message: 'You can not send messages to groups that you have not joined.', - code: 'GROUP_ACCESS_DENIED', - id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd', - }, - - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: '4372b8e2-185d-4146-8749-2f68864a3e5f', - }, - - contentRequired: { - message: 'Content required. You need to set text or fileId.', - code: 'CONTENT_REQUIRED', - id: '25587321-b0e6-449c-9239-f8925092942c', - }, - - youHaveBeenBlocked: { - message: 'You cannot send a message because you have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: 'c15a5199-7422-4968-941a-2a462c478f7d', - }, - }, + errors: ['ACCESS_DENIED', 'BLOCKED', 'NO_SUCH_FILE', 'NO_SUCH_USER', 'NO_SUCH_GROUP', 'RECIPIENT_IS_YOURSELF'], } as const; export const paramDef = { type: 'object', - properties: { - text: { type: 'string', nullable: true, maxLength: 3000 }, - fileId: { type: 'string', format: 'misskey:id' }, - }, anyOf: [ { properties: { - userId: { type: 'string', format: 'misskey:id' }, + text: { + type: 'string', + minLength: 1, + maxLength: 3000, + }, + fileId: { + type: 'string', + format: 'misskey:id', + }, + userId: { + type: 'string', + format: 'misskey:id', + }, }, - required: ['userId'], + required: ['text', 'userId'], }, { properties: { - groupId: { type: 'string', format: 'misskey:id' }, + fileId: { + type: 'string', + format: 'misskey:id', + }, + userId: { + type: 'string', + format: 'misskey:id', + }, }, - required: ['groupId'], + required: ['fileId', 'userId'], + }, + { + properties: { + text: { + type: 'string', + minLength: 1, + maxLength: 3000, + }, + fileId: { + type: 'string', + format: 'misskey:id', + }, + groupId: { + type: 'string', + format: 'misskey:id', + }, + }, + required: ['text', 'groupId'], + }, + { + properties: { + fileId: { + type: 'string', + format: 'misskey:id', + }, + groupId: { + type: 'string', + format: 'misskey:id', + }, + }, + required: ['fileId', 'groupId'], }, ], } as const; @@ -93,13 +97,11 @@ export default define(meta, paramDef, async (ps, user) => { if (ps.userId != null) { // Myself - if (ps.userId === user.id) { - throw new ApiError(meta.errors.recipientIsYourself); - } + if (ps.userId === user.id) throw new ApiError('RECIPIENT_IS_YOURSELF'); // Fetch recipient (user) recipientUser = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -108,16 +110,12 @@ export default define(meta, paramDef, async (ps, user) => { blockerId: recipientUser.id, blockeeId: user.id, }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } + if (block) throw new ApiError('BLOCKED'); } else if (ps.groupId != null) { // Fetch recipient (group) recipientGroup = await UserGroups.findOneBy({ id: ps.groupId! }); - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } + if (recipientGroup == null) throw new ApiError('NO_SUCH_GROUP'); // check joined const joining = await UserGroupJoinings.findOneBy({ @@ -125,9 +123,7 @@ export default define(meta, paramDef, async (ps, user) => { userGroupId: recipientGroup.id, }); - if (joining == null) { - throw new ApiError(meta.errors.groupAccessDenied); - } + if (joining == null) throw new ApiError('ACCESS_DENIED', 'You have to join a group to send a message in it.'); } let file = null; @@ -137,14 +133,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - // テキストが無いかつ添付ファイルも無かったらエラー - if (ps.text == null && file == null) { - throw new ApiError(meta.errors.contentRequired); + if (file == null) throw new ApiError('NO_SUCH_FILE'); } return await createMessage(user, recipientUser, recipientGroup, ps.text, file); diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts index 1997a8eda..4e86fa530 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts @@ -17,13 +17,7 @@ export const meta = { minInterval: SECOND, }, - errors: { - noSuchMessage: { - message: 'No such message.', - code: 'NO_SUCH_MESSAGE', - id: '54b5b326-7925-42cf-8019-130fda8b56af', - }, - }, + errors: ['NO_SUCH_MESSAGE'], } as const; export const paramDef = { @@ -41,9 +35,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } + if (message == null) throw new ApiError('NO_SUCH_MESSAGE'); await deleteMessage(message); }); diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts index c02b53469..3a3372c83 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts @@ -10,13 +10,7 @@ export const meta = { kind: 'write:messaging', - errors: { - noSuchMessage: { - message: 'No such message.', - code: 'NO_SUCH_MESSAGE', - id: '86d56a2f-a9c3-4afb-b13c-3e9bfef9aa14', - }, - }, + errors: ['NO_SUCH_MESSAGE'], } as const; export const paramDef = { @@ -31,18 +25,16 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const message = await MessagingMessages.findOneBy({ id: ps.messageId }); - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } + if (message == null) throw new ApiError('NO_SUCH_MESSAGE'); if (message.recipientId) { await readUserMessagingMessage(user.id, message.userId, [message.id]).catch(e => { - if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage); + if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError('NO_SUCH_MESSAGE'); throw e; }); } else if (message.groupId) { await readGroupMessagingMessage(user.id, message.groupId, [message.id]).catch(e => { - if (e.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage); + if (e.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError('NO_SUCH_MESSAGE'); throw e; }); } diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index f10fe0e2f..705fe9deb 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -13,25 +13,7 @@ export const meta = { kind: 'write:mutes', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '6fef56f3-e765-4957-88e5-c6f65329b8a5', - }, - - muteeIsYourself: { - message: 'Mutee is yourself.', - code: 'MUTEE_IS_YOURSELF', - id: 'a4619cb2-5f23-484b-9301-94c903074e10', - }, - - alreadyMuting: { - message: 'You are already muting that user.', - code: 'ALREADY_MUTING', - id: '7e7359cb-160c-4956-b08f-4d1c653cd007', - }, - }, + errors: ['NO_SUCH_USER', 'MUTEE_IS_YOURSELF', 'ALREADY_MUTING'], } as const; export const paramDef = { @@ -52,13 +34,11 @@ export default define(meta, paramDef, async (ps, user) => { const muter = user; // 自分自身 - if (user.id === ps.userId) { - throw new ApiError(meta.errors.muteeIsYourself); - } + if (user.id === ps.userId) throw new ApiError('MUTEE_IS_YOURSELF'); // Get mutee const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -68,9 +48,7 @@ export default define(meta, paramDef, async (ps, user) => { muteeId: mutee.id, }); - if (exist != null) { - throw new ApiError(meta.errors.alreadyMuting); - } + if (exist != null) throw new ApiError('ALREADY_MUTING'); if (ps.expiresAt && ps.expiresAt <= Date.now()) { return; diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index eed38528a..7585aeacb 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -11,25 +11,7 @@ export const meta = { kind: 'write:mutes', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'b851d00b-8ab1-4a56-8b1b-e24187cb48ef', - }, - - muteeIsYourself: { - message: 'Mutee is yourself.', - code: 'MUTEE_IS_YOURSELF', - id: 'f428b029-6b39-4d48-a1d2-cc1ae6dd5cf9', - }, - - notMuting: { - message: 'You are not muting that user.', - code: 'NOT_MUTING', - id: '5467d020-daa9-4553-81e1-135c0c35a96d', - }, - }, + errors: ['NO_SUCH_USER', 'MUTEE_IS_YOURSELF', 'NOT_MUTING'], } as const; export const paramDef = { @@ -45,13 +27,11 @@ export default define(meta, paramDef, async (ps, user) => { const muter = user; // Check if the mutee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.muteeIsYourself); - } + if (user.id === ps.userId) throw new ApiError('MUTEE_IS_YOURSELF'); // Get mutee const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -61,9 +41,7 @@ export default define(meta, paramDef, async (ps, user) => { muteeId: mutee.id, }); - if (exist == null) { - throw new ApiError(meta.errors.notMuting); - } + if (exist == null) throw new ApiError('NOT_MUTING'); // Delete mute await Mutings.delete({ diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 976c11260..e20b744a1 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -19,13 +19,7 @@ export const meta = { }, }, - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '47db1a1c-b0af-458d-8fb4-986e4efafe1e', - }, - }, + errors: ['NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -39,7 +33,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, me) => { const note = await getNote(ps.noteId, me).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index 7ee052001..b4cbc55f0 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -19,13 +19,7 @@ export const meta = { }, }, - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'e1035875-9551-45ec-afa8-1ded1fcb53c8', - }, - }, + errors: ['NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -41,7 +35,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 97cbe6677..a52f38df0 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -37,55 +37,7 @@ export const meta = { }, }, - errors: { - noSuchRenoteTarget: { - message: 'No such renote target.', - code: 'NO_SUCH_RENOTE_TARGET', - id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4', - }, - - cannotReRenote: { - message: 'You can not Renote a pure Renote.', - code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE', - id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', - }, - - noSuchReplyTarget: { - message: 'No such reply target.', - code: 'NO_SUCH_REPLY_TARGET', - id: '749ee0f6-d3da-459a-bf02-282e2da4292c', - }, - - cannotReplyToPureRenote: { - message: 'You can not reply to a pure Renote.', - code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', - id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', - }, - - cannotCreateAlreadyExpiredPoll: { - message: 'Poll is already expired.', - code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', - id: '04da457d-b083-4055-9082-955525eda5a5', - }, - - noSuchChannel: { - message: 'No such channel.', - code: 'NO_SUCH_CHANNEL', - id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb', - }, - - youHaveBeenBlocked: { - message: 'You have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3', - }, - - lessRestrictiveVisibility: { - message: 'The visibility cannot be less restrictive than the parent note.', - code: 'LESS_RESTRICTIVE_VISIBILITY', - id: 'c8ab7a7a-8852-41e2-8b24-079bbaceb585', - }, - }, + errors: ['NO_SUCH_NOTE', 'PURE_RENOTE', 'EXPIRED_POLL', 'NO_SUCH_CHANNEL', 'BLOCKED', 'LESS_RESTRICTIVE_VISIBILITY'], } as const; export const paramDef = { @@ -199,17 +151,15 @@ export default define(meta, paramDef, async (ps, user) => { if (ps.renoteId != null) { // Fetch renote to note renote = await getNote(ps.renoteId, user).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchRenoteTarget); + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE', 'Note to be renoted not found.'); throw e; }); - if (isPureRenote(renote)) { - throw new ApiError(meta.errors.cannotReRenote); - } + if (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)) { - throw new ApiError(meta.errors.lessRestrictiveVisibility); + throw new ApiError('LESS_RESTRICTIVE_VISIBILITY', `The renote has visibility ${renote.visibility}.`); } // Check blocking @@ -218,9 +168,7 @@ export default define(meta, paramDef, async (ps, user) => { blockerId: renote.userId, blockeeId: user.id, }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } + if (block) throw new ApiError('BLOCKED', 'Blocked by author of note to be renoted.'); } } @@ -228,17 +176,15 @@ export default define(meta, paramDef, async (ps, user) => { if (ps.replyId != null) { // Fetch reply reply = await getNote(ps.replyId, user).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchReplyTarget); + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE', 'Replied to note not found.'); throw e; }); - if (isPureRenote(reply)) { - throw new ApiError(meta.errors.cannotReplyToPureRenote); - } + if (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)) { - throw new ApiError(meta.errors.lessRestrictiveVisibility); + throw new ApiError('LESS_RESTRICTIVE_VISIBILITY', `The replied to note has visibility ${reply.visibility}.`); } // Check blocking @@ -247,16 +193,14 @@ export default define(meta, paramDef, async (ps, user) => { blockerId: reply.userId, blockeeId: user.id, }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } + if (block) throw new ApiError('BLOCKED', 'Blocked by author of replied to note.'); } } if (ps.poll) { if (typeof ps.poll.expiresAt === 'number') { if (ps.poll.expiresAt < Date.now()) { - throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + throw new ApiError('EXPIRED_POLL'); } } else if (typeof ps.poll.expiredAfter === 'number') { ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter; @@ -267,9 +211,7 @@ export default define(meta, paramDef, async (ps, user) => { if (ps.channelId != null) { channel = await Channels.findOneBy({ id: ps.channelId }); - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } + if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); } // 投稿を作成 diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 8aa1af85e..f33d04782 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -18,19 +18,7 @@ export const meta = { minInterval: SECOND, }, - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '490be23f-8c1f-4796-819f-94cb4f9d1630', - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: 'fe8d7103-0ea8-4ec3-814d-f8b401dc69e9', - }, - }, + errors: ['ACCESS_DENIED', 'NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -44,12 +32,12 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); if ((!user.isAdmin && !user.isModerator) && (note.userId !== user.id)) { - throw new ApiError(meta.errors.accessDenied); + throw new ApiError('ACCESS_DENIED'); } // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index b5dd88a4e..ffcb9b9e2 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -11,19 +11,7 @@ export const meta = { kind: 'write:favorites', - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '6dd26674-e060-4816-909a-45ba3f4da458', - }, - - alreadyFavorited: { - message: 'The note has already been marked as a favorite.', - code: 'ALREADY_FAVORITED', - id: 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6', - }, - }, + errors: ['NO_SUCH_NOTE', 'ALREADY_FAVORITED'], } as const; export const paramDef = { @@ -38,7 +26,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { // Get favoritee const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); @@ -48,9 +36,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (exist != null) { - throw new ApiError(meta.errors.alreadyFavorited); - } + if (exist != null) throw new ApiError('ALREADY_FAVORITED'); // Create favorite await NoteFavorites.insert({ diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index 3f4d39254..7c590c403 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -10,19 +10,7 @@ export const meta = { kind: 'write:favorites', - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '80848a2c-398f-4343-baa9-df1d57696c56', - }, - - notFavorited: { - message: 'You have not marked that note a favorite.', - code: 'NOT_FAVORITED', - id: 'b625fc69-635e-45e9-86f4-dbefbef35af5', - }, - }, + errors: ['NO_SUCH_NOTE', 'NOT_FAVORITED'], } as const; export const paramDef = { @@ -37,7 +25,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { // Get favoritee const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); @@ -47,9 +35,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (exist == null) { - throw new ApiError(meta.errors.notFavorited); - } + if (exist == null) throw new ApiError('NOT_FAVORITED'); // Delete favorite await NoteFavorites.delete(exist.id); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 3d32e7c7a..3cc8d292a 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -23,13 +23,7 @@ export const meta = { }, }, - errors: { - gtlDisabled: { - message: 'Global timeline has been disabled.', - code: 'GTL_DISABLED', - id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b', - }, - }, + errors: ['TIMELINE_DISABLED'], } as const; export const paramDef = { @@ -54,7 +48,7 @@ export default define(meta, paramDef, async (ps, user) => { const m = await fetchMeta(); if (m.disableGlobalTimeline) { if (user == null || (!user.isAdmin && !user.isModerator)) { - throw new ApiError(meta.errors.gtlDisabled); + throw new ApiError('TIMELINE_DISABLED'); } } diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index f2e86915f..18724c981 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -28,13 +28,7 @@ export const meta = { }, }, - errors: { - stlDisabled: { - message: 'Hybrid timeline has been disabled.', - code: 'STL_DISABLED', - id: '620763f4-f621-4533-ab33-0577a1a3c342', - }, - }, + errors: ['TIMELINE_DISABLED'], } as const; export const paramDef = { @@ -61,7 +55,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const m = await fetchMeta(); if (m.disableLocalTimeline && (!user.isAdmin && !user.isModerator)) { - throw new ApiError(meta.errors.stlDisabled); + throw new ApiError('TIMELINE_DISABLED'); } //#region Construct query diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 6a65c028a..2e13fb432 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -26,13 +26,7 @@ export const meta = { }, }, - errors: { - ltlDisabled: { - message: 'Local timeline has been disabled.', - code: 'LTL_DISABLED', - id: '45a6eb02-7695-4393-b023-dd3be9aaaefd', - }, - }, + errors: ['TIMELINE_DISABLED'], } as const; export const paramDef = { @@ -61,7 +55,7 @@ export default define(meta, paramDef, async (ps, user) => { const m = await fetchMeta(); if (m.disableLocalTimeline) { if (user == null || (!user.isAdmin && !user.isModerator)) { - throw new ApiError(meta.errors.ltlDisabled); + throw new ApiError('TIMELINE_DISABLED'); } } diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 9b4154452..10d329c9d 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -1,5 +1,6 @@ import { Brackets } from 'typeorm'; -import read from '@/services/note/read.js'; +import { noteVisibilities } from 'foundkey-js'; +import { readNote } from '@/services/note/read.js'; import { Notes, Followings } from '@/models/index.js'; import define from '../../define.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; @@ -31,7 +32,10 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, - visibility: { type: 'string' }, + visibility: { + type: 'string', + enum: noteVisibilities, + }, }, required: [], } as const; @@ -75,7 +79,7 @@ export default define(meta, paramDef, async (ps, user) => { const mentions = await query.take(ps.limit).getMany(); - read(user.id, mentions); + readNote(user.id, mentions); return await Notes.packMany(mentions, user); }); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 331b03971..6648e996b 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -19,50 +19,14 @@ export const meta = { kind: 'write:votes', - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'ecafbd2e-c283-4d6d-aecb-1a0a33b75396', - }, - - noPoll: { - message: 'The note does not attach a poll.', - code: 'NO_POLL', - id: '5f979967-52d9-4314-a911-1c673727f92f', - }, - - invalidChoice: { - message: 'Choice ID is invalid.', - code: 'INVALID_CHOICE', - id: 'e0cc9a04-f2e8-41e4-a5f1-4127293260cc', - }, - - alreadyVoted: { - message: 'You have already voted.', - code: 'ALREADY_VOTED', - id: '0963fc77-efac-419b-9424-b391608dc6d8', - }, - - alreadyExpired: { - message: 'The poll is already expired.', - code: 'ALREADY_EXPIRED', - id: '1022a357-b085-4054-9083-8f8de358337e', - }, - - youHaveBeenBlocked: { - message: 'You cannot vote this poll because you have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: '85a5377e-b1e9-4617-b0b9-5bea73331e49', - }, - }, + errors: ['NO_SUCH_NOTE', 'INVALID_CHOICE', 'ALREADY_VOTED', 'EXPIRED_POLL', 'BLOCKED'], } as const; export const paramDef = { type: 'object', properties: { noteId: { type: 'string', format: 'misskey:id' }, - choice: { type: 'integer' }, + choice: { type: 'integer', minimum: 0 }, }, required: ['noteId', 'choice'], } as const; @@ -73,12 +37,12 @@ export default define(meta, paramDef, async (ps, user) => { // Get votee const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); if (!note.hasPoll) { - throw new ApiError(meta.errors.noPoll); + throw new ApiError('NO_SUCH_NOTE', 'The note exists but does not have a poll attached.'); } // Check blocking @@ -87,19 +51,17 @@ export default define(meta, paramDef, async (ps, user) => { blockerId: note.userId, blockeeId: user.id, }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } + if (block) throw new ApiError('BLOCKED'); } const poll = await Polls.findOneByOrFail({ noteId: note.id }); if (poll.expiresAt && poll.expiresAt < createdAt) { - throw new ApiError(meta.errors.alreadyExpired); + throw new ApiError('EXPIRED_POLL'); } if (poll.choices[ps.choice] == null) { - throw new ApiError(meta.errors.invalidChoice); + throw new ApiError('INVALID_CHOICE', `There are only ${poll.choices.length} choices.`); } // if already voted @@ -111,10 +73,10 @@ export default define(meta, paramDef, async (ps, user) => { if (exist.length) { if (poll.multiple) { if (exist.some(x => x.choice === ps.choice)) { - throw new ApiError(meta.errors.alreadyVoted); + throw new ApiError('ALREADY_VOTED', 'This is a multiple choice poll, but you already voted for that option.'); } } else { - throw new ApiError(meta.errors.alreadyVoted); + throw new ApiError('ALREADY_VOTED', 'This is a single choice poll.'); } } diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index d9388b47f..d7a63c056 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -23,20 +23,18 @@ export const meta = { }, }, - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '263fff3d-d0e1-4af4-bea7-8408059b451a', - }, - }, + errors: ['NO_SUCH_NOTE'], } as const; export const paramDef = { type: 'object', properties: { noteId: { type: 'string', format: 'misskey:id' }, - type: { type: 'string', nullable: true }, + type: { + description: 'A Unicode emoji or custom emoji code. A custom emoji should look like `:name:` or `:name@example.com:`.', + type: 'string', + nullable: true, + }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -49,7 +47,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { // check note visibility const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index b5c0c9d17..d99dc314e 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -1,4 +1,4 @@ -import createReaction from '@/services/note/reaction/create.js'; +import { createReaction } from '@/services/note/reaction/create.js'; import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; @@ -10,25 +10,7 @@ export const meta = { kind: 'write:reactions', - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '033d0620-5bfe-4027-965d-980b0c85a3ea', - }, - - alreadyReacted: { - message: 'You are already reacting to that note.', - code: 'ALREADY_REACTED', - id: '71efcf98-86d6-4e2b-b2ad-9d032369366b', - }, - - youHaveBeenBlocked: { - message: 'You cannot react this note because you have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: '20ef5475-9f38-4e4c-bd33-de6d979498ec', - }, - }, + errors: ['NO_SUCH_NOTE', 'ALREADY_REACTED', 'BLOCKED'], } as const; export const paramDef = { @@ -43,12 +25,12 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); await createReaction(user, note, ps.reaction).catch(e => { - if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); - if (e.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked); + if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError('ALREADY_REACTED'); + if (e.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError('BLOCKED'); throw e; }); return; 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 e1bb63197..b974fc3ef 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,4 +1,4 @@ -import deleteReaction from '@/services/note/reaction/delete.js'; +import { deleteReaction } from '@/services/note/reaction/delete.js'; import { SECOND, HOUR } from '@/const.js'; import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; @@ -17,19 +17,7 @@ export const meta = { minInterval: 3 * SECOND, }, - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '764d9fce-f9f2-4a0e-92b1-6ceac9a7ad37', - }, - - notReacted: { - message: 'You are not reacting to that note.', - code: 'NOT_REACTED', - id: '92f4426d-4196-4125-aa5b-02943e2ec8fc', - }, - }, + errors: ['NO_SUCH_NOTE', 'NOT_REACTED'], } as const; export const paramDef = { @@ -43,11 +31,11 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); await deleteReaction(user, note).catch(e => { - if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted); + if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError('NOT_REACTED'); throw e; }); }); diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 1fa9c5230..a0402cf2f 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -22,13 +22,7 @@ export const meta = { }, }, - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '12908022-2e21-46cd-ba6a-3edaf6093f46', - }, - }, + errors: ['NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -45,7 +39,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 1d393f796..e75fe0ef6 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -22,9 +22,6 @@ export const meta = { ref: 'Note', }, }, - - errors: { - }, } as const; export const paramDef = { @@ -53,6 +50,8 @@ export default define(meta, paramDef, async (ps, me) => { if (ps.userId) { query.andWhere('note.userId = :userId', { userId: ps.userId }); + } else if (ps.host) { + query.andWhere('note.userHost = :host', { host: ps.host }); } else if (ps.channelId) { query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); } diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index c9c148747..b2dde8647 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -14,13 +14,7 @@ export const meta = { ref: 'Note', }, - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', - }, - }, + errors: ['NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -34,7 +28,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); @@ -42,7 +36,7 @@ export default define(meta, paramDef, async (ps, user) => { // FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774) detail: true, }).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); }); diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index d7599dc30..dddb55fcb 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -1,7 +1,7 @@ import { noteNotificationTypes } from 'foundkey-js'; import { Notes, NoteThreadMutings, NoteWatchings } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; -import readNote from '@/services/note/read.js'; +import { readNote } from '@/services/note/read.js'; import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; @@ -13,13 +13,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '5ff67ada-ed3b-2e71-8e87-a1a421e177d2', - }, - }, + errors: ['NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -41,7 +35,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index cbc0e5ce5..c1ea564eb 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -10,13 +10,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a', - }, - }, + errors: ['NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -30,7 +24,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index a5eca5e99..f62e07b4e 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -8,6 +8,36 @@ import { ApiError } from '../../error.js'; import { getNote } from '../../common/getters.js'; import define from '../../define.js'; +const sourceLangs = [ + 'BG', + 'CS', + 'DA', + 'DE', + 'EL', + 'EN', + 'ES', + 'ET', + 'FI', + 'FR', + 'HU', + 'ID', + 'IT', + 'JA', + 'LT', + 'LV', + 'NL', + 'PL', + 'PT', + 'RO', + 'RU', + 'SK', + 'SL', + 'SV', + 'TR', + 'UK', + 'ZH', +]; + export const meta = { tags: ['notes'], @@ -16,15 +46,16 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'bea9b03f-36e0-49c5-a4db-627a029f8971', + properties: { + sourceLang: { + type: 'string', + enum: sourceLangs, + }, + text: { type: 'string' }, }, }, + + errors: ['NO_SUCH_NOTE'], } as const; // List of permitted languages from https://www.deepl.com/docs-api/translate-text/translate-text/ @@ -34,35 +65,7 @@ export const paramDef = { noteId: { type: 'string', format: 'misskey:id' }, sourceLang: { type: 'string', - enum: [ - 'BG', - 'CS', - 'DA', - 'DE', - 'EL', - 'EN', - 'ES', - 'ET', - 'FI', - 'FR', - 'HU', - 'ID', - 'IT', - 'JA', - 'LT', - 'LV', - 'NL', - 'PL', - 'PT', - 'RO', - 'RU', - 'SK', - 'SL', - 'SV', - 'TR', - 'UK', - 'ZH', - ], + enum: sourceLangs, }, targetLang: { type: 'string', @@ -107,7 +110,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 68fab7889..b7bb3009a 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -18,13 +18,7 @@ export const meta = { minInterval: SECOND, }, - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'efd4a259-2442-496b-8dd7-b255aa1a160f', - }, - }, + errors: ['NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -38,7 +32,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index e603a8f62..c95efd6b6 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -21,13 +21,7 @@ export const meta = { }, }, - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '8fb1fbd5-e476-4c37-9fb0-43d55b63a2ff', - }, - }, + errors: ['NO_SUCH_USER_LIST'], } as const; export const paramDef = { @@ -58,9 +52,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (list == null) { - throw new ApiError(meta.errors.noSuchList); - } + if (list == null) throw new ApiError('NO_SUCH_USER_LIST'); //#region Construct query const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) diff --git a/packages/backend/src/server/api/endpoints/notes/watching/create.ts b/packages/backend/src/server/api/endpoints/notes/watching/create.ts index 6025799fa..002e9b007 100644 --- a/packages/backend/src/server/api/endpoints/notes/watching/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/watching/create.ts @@ -1,4 +1,4 @@ -import watch from '@/services/note/watch.js'; +import { watch } from '@/services/note/watch.js'; import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; @@ -10,13 +10,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'ea0e37a6-90a3-4f58-ba6b-c328ca206fc7', - }, - }, + errors: ['NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -30,7 +24,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts index 7021c7970..54f7163b9 100644 --- a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts @@ -1,4 +1,4 @@ -import unwatch from '@/services/note/unwatch.js'; +import { unwatch } from '@/services/note/unwatch.js'; import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; @@ -10,13 +10,7 @@ export const meta = { kind: 'write:account', - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: '09b3695c-f72c-4731-a428-7cff825fc82e', - }, - }, + errors: ['NO_SUCH_NOTE'], } as const; export const paramDef = { @@ -30,7 +24,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index 80d513d8d..e45ebdd1d 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -7,9 +7,6 @@ export const meta = { requireCredential: true, kind: 'write:notifications', - - errors: { - }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts index 7bce525a5..f7b64fddb 100644 --- a/packages/backend/src/server/api/endpoints/notifications/read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/read.ts @@ -10,13 +10,8 @@ export const meta = { description: 'Mark a notification as read.', - errors: { - noSuchNotification: { - message: 'No such notification.', - code: 'NO_SUCH_NOTIFICATION', - id: 'efa929d5-05b5-47d1-beec-e6a4dbed011e', - }, - }, + // FIXME: This error makes sense here but will never be thrown here. + // errors: ['NO_SUCH_NOTIFICATION'], } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index 6dd3ede85..e69b279ea 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -7,13 +7,7 @@ export const meta = { requireCredential: true, secure: true, - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: '4a13ad31-6729-46b4-b9af-e86b265c2e74', - }, - }, + errors: ['NO_SUCH_PAGE'], } as const; export const paramDef = { @@ -29,9 +23,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } + if (page == null) throw new ApiError('NO_SUCH_PAGE'); publishMainStream(page.userId, 'pageEvent', { pageId: ps.pageId, diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index 8eafe556c..7649792cd 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -23,18 +23,7 @@ export const meta = { ref: 'Page', }, - errors: { - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'b7b97489-0f66-4b12-a5ff-b21bd63f6e1c', - }, - nameAlreadyExists: { - message: 'Specified name already exists.', - code: 'NAME_ALREADY_EXISTS', - id: '4650348e-301c-499a-83c9-6aa988c66bc1', - }, - }, + errors: ['NO_SUCH_FILE', 'NAME_ALREADY_EXISTS'], } as const; export const paramDef = { @@ -61,9 +50,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (eyeCatchingImage == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (eyeCatchingImage == null) throw new ApiError('NO_SUCH_FILE'); } await Pages.findBy({ @@ -71,7 +58,7 @@ export default define(meta, paramDef, async (ps, user) => { name: ps.name, }).then(result => { if (result.length > 0) { - throw new ApiError(meta.errors.nameAlreadyExists); + throw new ApiError('NAME_ALREADY_EXISTS'); } }); diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index a7708e658..b8f8d43c7 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -9,19 +9,7 @@ export const meta = { kind: 'write:pages', - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: 'eb0c6e1d-d519-4764-9486-52a7e1c6392a', - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '8b741b3e-2c22-44b3-a15f-29949aa1601e', - }, - }, + errors: ['NO_SUCH_PAGE'], } as const; export const paramDef = { @@ -34,13 +22,11 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - if (page.userId !== user.id) { - throw new ApiError(meta.errors.accessDenied); - } + const page = await Pages.findOneBy({ + id: ps.pageId, + userId: user.id, + }); + if (page == null) throw new ApiError('NO_SUCH_PAGE'); await Pages.delete(page.id); }); diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 269b539f7..19b0d99ed 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -10,25 +10,7 @@ export const meta = { kind: 'write:page-likes', - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3', - }, - - yourPage: { - message: 'You cannot like your page.', - code: 'YOUR_PAGE', - id: '28800466-e6db-40f2-8fae-bf9e82aa92b8', - }, - - alreadyLiked: { - message: 'The page has already been liked.', - code: 'ALREADY_LIKED', - id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3', - }, - }, + errors: ['ALREADY_LIKED', 'NO_SUCH_PAGE'], } as const; export const paramDef = { @@ -42,13 +24,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - - if (page.userId === user.id) { - throw new ApiError(meta.errors.yourPage); - } + if (page == null) throw new ApiError('NO_SUCH_PAGE'); // if already liked const exist = await PageLikes.findOneBy({ @@ -56,9 +32,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (exist != null) { - throw new ApiError(meta.errors.alreadyLiked); - } + if (exist != null) throw new ApiError('ALREADY_LIKED'); // Create like await PageLikes.insert({ diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index 5d37e86b9..9861efdaa 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -15,13 +15,7 @@ export const meta = { ref: 'Page', }, - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: '222120c0-3ead-4528-811b-b96f233388d7', - }, - }, + errors: ['NO_SUCH_PAGE'], } as const; export const paramDef = { @@ -62,9 +56,7 @@ export default define(meta, paramDef, async (ps, user) => { } } - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } + if (page == null) throw new ApiError('NO_SUCH_PAGE'); return await Pages.pack(page, user); }); diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index 6b3a2bec1..5c18312d3 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -9,19 +9,7 @@ export const meta = { kind: 'write:page-likes', - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: 'a0d41e20-1993-40bd-890e-f6e560ae648e', - }, - - notLiked: { - message: 'You have not liked that page.', - code: 'NOT_LIKED', - id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee', - }, - }, + errors: ['NO_SUCH_PAGE', 'NOT_LIKED'], } as const; export const paramDef = { @@ -35,18 +23,14 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } + if (page == null) throw new ApiError('NO_SUCH_PAGE'); const exist = await PageLikes.findOneBy({ pageId: page.id, userId: user.id, }); - if (exist == null) { - throw new ApiError(meta.errors.notLiked); - } + if (exist == null) throw new ApiError('NOT_LIKED'); // Delete like await PageLikes.delete(exist.id); diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index 319af3b88..f82f754d1 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -16,30 +16,7 @@ export const meta = { max: 300, }, - errors: { - noSuchPage: { - message: 'No such page.', - code: 'NO_SUCH_PAGE', - id: '21149b9e-3616-4778-9592-c4ce89f5a864', - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '3c15cd52-3b4b-4274-967d-6456fc4f792b', - }, - - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'cfc23c7c-3887-490e-af30-0ed576703c82', - }, - nameAlreadyExists: { - message: 'Specified name already exists.', - code: 'NAME_ALREADY_EXISTS', - id: '2298a392-d4a1-44c5-9ebb-ac1aeaa5a9ab', - }, - }, + errors: ['NAME_ALREADY_EXISTS', 'NO_SUCH_FILE', 'NO_SUCH_PAGE'], } as const; export const paramDef = { @@ -60,13 +37,11 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - if (page.userId !== user.id) { - throw new ApiError(meta.errors.accessDenied); - } + const page = await Pages.findOneBy({ + id: ps.pageId, + userId: user.id, + }); + if (page == null) throw new ApiError('NO_SUCH_PAGE'); let eyeCatchingImage = null; if (ps.eyeCatchingImageId != null) { @@ -75,9 +50,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (eyeCatchingImage == null) { - throw new ApiError(meta.errors.noSuchFile); - } + if (eyeCatchingImage == null) throw new ApiError('NO_SUCH_FILE'); } await Pages.findBy({ @@ -85,9 +58,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, name: ps.name, }).then(result => { - if (result.length > 0) { - throw new ApiError(meta.errors.nameAlreadyExists); - } + if (result.length > 0) throw new ApiError('NAME_ALREADY_EXISTS'); }); await Pages.update(page.id, { diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts index e1db204b7..9320c4e2c 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -13,25 +13,7 @@ export const meta = { kind: 'write:mutes', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '6fef56f3-e765-4957-88e5-c6f65329b8a5', - }, - - muteeIsYourself: { - message: 'Mutee is yourself.', - code: 'MUTEE_IS_YOURSELF', - id: 'a4619cb2-5f23-484b-9301-94c903074e10', - }, - - alreadyMuting: { - message: 'You are already muting that user.', - code: 'ALREADY_MUTING', - id: '7e7359cb-160c-4956-b08f-4d1c653cd007', - }, - }, + errors: ['NO_SUCH_USER', 'MUTEE_IS_YOURSELF', 'ALREADY_MUTING'], } as const; export const paramDef = { @@ -47,13 +29,11 @@ export default define(meta, paramDef, async (ps, user) => { const muter = user; // Check if the mutee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.muteeIsYourself); - } + if (user.id === ps.userId) throw new ApiError('MUTEE_IS_YOURSELF'); // Get mutee const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -63,9 +43,7 @@ export default define(meta, paramDef, async (ps, user) => { muteeId: mutee.id, }); - if (exist != null) { - throw new ApiError(meta.errors.alreadyMuting); - } + if (exist != null) throw new ApiError('ALREADY_MUTING'); // Create mute await RenoteMutings.insert({ diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts index bdfaae263..0f7c4cde7 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts @@ -11,25 +11,7 @@ export const meta = { kind: 'write:mutes', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'b851d00b-8ab1-4a56-8b1b-e24187cb48ef', - }, - - muteeIsYourself: { - message: 'Mutee is yourself.', - code: 'MUTEE_IS_YOURSELF', - id: 'f428b029-6b39-4d48-a1d2-cc1ae6dd5cf9', - }, - - notMuting: { - message: 'You are not muting that user.', - code: 'NOT_MUTING', - id: '5467d020-daa9-4553-81e1-135c0c35a96d', - }, - }, + errors: ['NO_SUCH_USER', 'MUTEE_IS_YOURSELF', 'NOT_MUTING'], } as const; export const paramDef = { @@ -45,13 +27,11 @@ export default define(meta, paramDef, async (ps, user) => { const muter = user; // Check if the mutee is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.muteeIsYourself); - } + if (user.id === ps.userId) throw new ApiError('MUTEE_IS_YOURSELF'); // Get mutee const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -61,9 +41,7 @@ export default define(meta, paramDef, async (ps, user) => { muteeId: mutee.id, }); - if (exist == null) { - throw new ApiError(meta.errors.notMuting); - } + if (exist == null) throw new ApiError('NOT_MUTING'); // Delete mute await RenoteMutings.delete({ diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 3aae59e33..4ca4703e9 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -4,7 +4,7 @@ import config from '@/config/index.js'; import { Users, UserProfiles, PasswordResetRequests } from '@/models/index.js'; import { sendEmail } from '@/services/send-email.js'; import { genId } from '@/misc/gen-id.js'; -import { HOUR } from '@/const.js'; +import { DAY } from '@/const.js'; import define from '../define.js'; export const meta = { @@ -15,12 +15,8 @@ export const meta = { description: 'Request a users password to be reset.', limit: { - duration: HOUR, - max: 3, - }, - - errors: { - + duration: DAY, + max: 1, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 7f17339e9..1d145c31d 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -7,10 +7,6 @@ export const meta = { requireCredential: false, description: 'Only available when running with NODE_ENV=testing. Reset the database and flush Redis.', - - errors: { - - }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index ea7df52c0..51b26a2b0 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,5 +1,6 @@ import bcrypt from 'bcryptjs'; import { UserProfiles, PasswordResetRequests } from '@/models/index.js'; +import { DAY, MINUTE } from '@/const.js'; import define from '../define.js'; export const meta = { @@ -9,9 +10,12 @@ export const meta = { description: 'Complete the password reset that was previously requested.', - errors: { - + limit: { + duration: DAY, + max: 1, }, + + errors: ['NO_SUCH_RESET_REQUEST'], } as const; export const paramDef = { @@ -25,13 +29,18 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const req = await PasswordResetRequests.findOneByOrFail({ + const req = await PasswordResetRequests.findOneBy({ token: ps.token, }); + if (req == null) throw new ApiError('NO_SUCH_RESET_REQUEST'); - // 発行してから30分以上経過していたら無効 - if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) { - throw new Error(); // TODO + // expires after 30 minutes + // This is a secondary check just in case the expiry task is broken, + // the expiry task is badly aligned with this expiration or something + // else strange is going on. + if (Date.now() - req.createdAt.getTime() > 30 * MINUTE) { + await PasswordResetRequests.delete(req.id); + throw new ApiError('NO_SUCH_RESET_REQUEST'); } // Generate hash of password @@ -42,5 +51,5 @@ export default define(meta, paramDef, async (ps, user) => { password: hash, }); - PasswordResetRequests.delete(req.id); + await PasswordResetRequests.delete(req.id); }); diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts deleted file mode 100644 index 9949237a7..000000000 --- a/packages/backend/src/server/api/endpoints/test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import define from '../define.js'; - -export const meta = { - tags: ['non-productive'], - - description: 'Endpoint for testing input validation.', - - requireCredential: false, -} as const; - -export const paramDef = { - type: 'object', - properties: { - required: { type: 'boolean' }, - string: { type: 'string' }, - default: { type: 'string', default: 'hello' }, - nullableDefault: { type: 'string', nullable: true, default: 'hello' }, - id: { type: 'string', format: 'misskey:id' }, - }, - required: ['required'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - return ps; -}); diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 7f9f98076..e395c3f79 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -22,19 +22,7 @@ export const meta = { }, }, - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '27fa5435-88ab-43de-9360-387de88727cd', - }, - - forbidden: { - message: 'Forbidden.', - code: 'FORBIDDEN', - id: '3c6a84db-d619-26af-ca14-06232a21df8a', - }, - }, + errors: ['NO_SUCH_USER', 'ACCESS_DENIED'], } as const; export const paramDef = { @@ -71,26 +59,24 @@ export default define(meta, paramDef, async (ps, me) => { ? { id: ps.userId } : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) ?? IsNull() }); - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } + if (user == null) throw new ApiError('NO_SUCH_USER'); const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); if (profile.ffVisibility === 'private') { if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); + throw new ApiError('ACCESS_DENIED'); } } else if (profile.ffVisibility === 'followers') { if (me == null) { - throw new ApiError(meta.errors.forbidden); + throw new ApiError('ACCESS_DENIED'); } else if (me.id !== user.id) { const following = await Followings.findOneBy({ followeeId: user.id, followerId: me.id, }); if (following == null) { - throw new ApiError(meta.errors.forbidden); + throw new ApiError('ACCESS_DENIED'); } } } diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 0aaa810f7..51ad75a77 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -22,19 +22,7 @@ export const meta = { }, }, - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '63e4aba4-4156-4e53-be25-c9559e42d71b', - }, - - forbidden: { - message: 'Forbidden.', - code: 'FORBIDDEN', - id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba', - }, - }, + errors: ['ACCESS_DENIED', 'NO_SUCH_USER'], } as const; export const paramDef = { @@ -71,26 +59,24 @@ export default define(meta, paramDef, async (ps, me) => { ? { id: ps.userId } : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) ?? IsNull() }); - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } + if (user == null) throw new ApiError('NO_SUCH_USER'); const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); if (profile.ffVisibility === 'private') { if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); + throw new ApiError('ACCESS_DENIED'); } } else if (profile.ffVisibility === 'followers') { if (me == null) { - throw new ApiError(meta.errors.forbidden); + throw new ApiError('ACCESS_DENIED'); } else if (me.id !== user.id) { const following = await Followings.findOneBy({ followeeId: user.id, followerId: me.id, }); if (following == null) { - throw new ApiError(meta.errors.forbidden); + throw new ApiError('ACCESS_DENIED'); } } } diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts deleted file mode 100644 index 56965d306..000000000 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Not, In, IsNull } from 'typeorm'; -import { maximum } from '@/prelude/array.js'; -import { Notes, Users } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; - -export const meta = { - tags: ['users'], - - requireCredential: false, - - description: 'Get a list of other users that the specified user frequently replies to.', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - user: { - type: 'object', - optional: false, nullable: false, - ref: 'UserDetailed', - }, - weight: { - type: 'number', - optional: false, nullable: false, - }, - }, - }, - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'e6965129-7b2a-40a4-bae2-cd84cd434822', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - // Lookup user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Fetch recent notes - const recentNotes = await Notes.find({ - where: { - userId: user.id, - replyId: Not(IsNull()), - }, - order: { - id: -1, - }, - take: 1000, - select: ['replyId'], - }); - - // 投稿が少なかったら中断 - if (recentNotes.length === 0) { - return []; - } - - // TODO ミュートを考慮 - const replyTargetNotes = await Notes.find({ - where: { - id: In(recentNotes.map(p => p.replyId)), - }, - select: ['userId'], - }); - - const repliedUsers: any = {}; - - // Extract replies from recent notes - for (const userId of replyTargetNotes.map(x => x.userId.toString())) { - if (repliedUsers[userId]) { - repliedUsers[userId]++; - } else { - repliedUsers[userId] = 1; - } - } - - // Calc peak - const peak = maximum(Object.values(repliedUsers)); - - // Sort replies by frequency - const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); - - // Extract top replied users - const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit); - - // Make replies object (includes weights) - const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ - user: await Users.pack(user, me, { detail: true }), - weight: repliedUsers[user] / peak, - }))); - - return repliesObj; -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts index 2ff1f9aec..8c9ff7134 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/delete.ts @@ -11,13 +11,7 @@ export const meta = { description: 'Delete an existing group.', - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '63dbd64c-cd77-413f-8e08-61781e210b38', - }, - }, + errors: ['NO_SUCH_GROUP'], } as const; export const paramDef = { @@ -35,9 +29,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } + if (userGroup == null) throw new ApiError('NO_SUCH_GROUP'); await UserGroups.delete(userGroup.id); }); diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts index 220fff5f3..88086a084 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -13,13 +13,7 @@ export const meta = { description: 'Join a group the authenticated user has been invited to.', - errors: { - noSuchInvitation: { - message: 'No such invitation.', - code: 'NO_SUCH_INVITATION', - id: '98c11eca-c890-4f42-9806-c8c8303ebb5e', - }, - }, + errors: ['NO_SUCH_INVITATION'], } as const; export const paramDef = { @@ -35,15 +29,10 @@ export default define(meta, paramDef, async (ps, user) => { // Fetch the invitation const invitation = await UserGroupInvitations.findOneBy({ id: ps.invitationId, + userId: user.id, }); - if (invitation == null) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - if (invitation.userId !== user.id) { - throw new ApiError(meta.errors.noSuchInvitation); - } + if (invitation == null) throw new ApiError('NO_SUCH_INVITATION'); // Push the user await UserGroupJoinings.insert({ diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts index 8d1d3db73..0f3712b02 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts @@ -11,13 +11,7 @@ export const meta = { description: 'Delete an existing group invitation for the authenticated user without joining the group.', - errors: { - noSuchInvitation: { - message: 'No such invitation.', - code: 'NO_SUCH_INVITATION', - id: 'ad7471d4-2cd9-44b4-ac68-e7136b4ce656', - }, - }, + errors: ['NO_SUCH_INVITATION'], } as const; export const paramDef = { @@ -33,15 +27,10 @@ export default define(meta, paramDef, async (ps, user) => { // Fetch the invitation const invitation = await UserGroupInvitations.findOneBy({ id: ps.invitationId, + userId: user.id, }); - if (invitation == null) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - if (invitation.userId !== user.id) { - throw new ApiError(meta.errors.noSuchInvitation); - } + if (invitation == null) throw new ApiError('NO_SUCH_INVITATION'); await UserGroupInvitations.delete(invitation.id); }); diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts index 1a8d320f3..7a792a49b 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -15,31 +15,7 @@ export const meta = { description: 'Invite a user to an existing group.', - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '583f8bc0-8eee-4b78-9299-1e14fc91e409', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'da52de61-002c-475b-90e1-ba64f9cf13a8', - }, - - alreadyAdded: { - message: 'That user has already been added to that group.', - code: 'ALREADY_ADDED', - id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c', - }, - - alreadyInvited: { - message: 'That user has already been invited to that group.', - code: 'ALREADY_INVITED', - id: 'ee0f58b4-b529-4d13-b761-b9a3e69f97e6', - }, - }, + errors: ['NO_SUCH_USER', 'NO_SUCH_GROUP', 'ALREADY_ADDED', 'ALREADY_INVITED'], } as const; export const paramDef = { @@ -59,13 +35,11 @@ export default define(meta, paramDef, async (ps, me) => { userId: me.id, }); - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } + if (userGroup == null) throw new ApiError('NO_SUCH_GROUP'); // Fetch the user const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -74,18 +48,14 @@ export default define(meta, paramDef, async (ps, me) => { userId: user.id, }); - if (joining) { - throw new ApiError(meta.errors.alreadyAdded); - } + if (joining) throw new ApiError('ALREADY_ADDED'); const existInvitation = await UserGroupInvitations.findOneBy({ userGroupId: userGroup.id, userId: user.id, }); - if (existInvitation) { - throw new ApiError(meta.errors.alreadyInvited); - } + if (existInvitation) throw new ApiError('ALREADY_INVITED'); const invitation = await UserGroupInvitations.insert({ id: genId(), diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts index 83dc757db..19a4982bb 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/leave.ts @@ -11,19 +11,7 @@ export const meta = { description: 'Leave a group. The owner of a group can not leave. They must transfer ownership or delete the group instead.', - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '62780270-1f67-5dc0-daca-3eb510612e31', - }, - - youAreOwner: { - message: 'Your are the owner.', - code: 'YOU_ARE_OWNER', - id: 'b6d6e0c2-ef8a-9bb8-653d-79f4a3107c69', - }, - }, + errors: ['NO_SUCH_GROUP', 'GROUP_OWNER'], } as const; export const paramDef = { @@ -41,13 +29,9 @@ export default define(meta, paramDef, async (ps, me) => { id: ps.groupId, }); - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } + if (userGroup == null) throw new ApiError('NO_SUCH_GROUP'); - if (me.id === userGroup.userId) { - throw new ApiError(meta.errors.youAreOwner); - } + if (me.id === userGroup.userId) throw new ApiError('GROUP_OWNER'); await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: me.id }); }); diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts index ba67a1e5c..30caceb27 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts @@ -12,25 +12,7 @@ export const meta = { description: 'Removes a specified user from a group. The owner can not be removed.', - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '4662487c-05b1-4b78-86e5-fd46998aba74', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '0b5cc374-3681-41da-861e-8bc1146f7a55', - }, - - isOwner: { - message: 'The user is the owner.', - code: 'IS_OWNER', - id: '1546eed5-4414-4dea-81c1-b0aec4f6d2af', - }, - }, + errors: ['NO_SUCH_USER', 'NO_SUCH_GROUP', 'GROUP_OWNER'], } as const; export const paramDef = { @@ -50,19 +32,15 @@ export default define(meta, paramDef, async (ps, me) => { userId: me.id, }); - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } + if (userGroup == null) throw new ApiError('NO_SUCH_GROUP'); // Fetch the user const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); - if (user.id === userGroup.userId) { - throw new ApiError(meta.errors.isOwner); - } + if (user.id === userGroup.userId) throw new ApiError('GROUP_OWNER'); // Pull the user await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: user.id }); diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts index 21e3d9da2..eddd933c2 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts @@ -17,13 +17,7 @@ export const meta = { ref: 'UserGroup', }, - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b', - }, - }, + errors: ['NO_SUCH_GROUP'], } as const; export const paramDef = { @@ -41,9 +35,7 @@ export default define(meta, paramDef, async (ps, me) => { id: ps.groupId, }); - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } + if (userGroup == null) throw new ApiError('NO_SUCH_GROUP'); const joining = await UserGroupJoinings.findOneBy({ userId: me.id, @@ -51,7 +43,7 @@ export default define(meta, paramDef, async (ps, me) => { }); if (joining == null && userGroup.userId !== me.id) { - throw new ApiError(meta.errors.noSuchGroup); + throw new ApiError('NO_SUCH_GROUP'); } return await UserGroups.pack(userGroup); diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts index 6456e70dd..b6607e4f8 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts @@ -18,25 +18,7 @@ export const meta = { ref: 'UserGroup', }, - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '8e31d36b-2f88-4ccd-a438-e2d78a9162db', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '711f7ebb-bbb9-4dfa-b540-b27809fed5e9', - }, - - noSuchGroupMember: { - message: 'No such group member.', - code: 'NO_SUCH_GROUP_MEMBER', - id: 'd31bebee-196d-42c2-9a3e-9474d4be6cc4', - }, - }, + errors: ['NO_SUCH_GROUP', 'NO_SUCH_USER'], } as const; export const paramDef = { @@ -56,13 +38,11 @@ export default define(meta, paramDef, async (ps, me) => { userId: me.id, }); - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } + if (userGroup == null) throw new ApiError('NO_SUCH_GROUP'); // Fetch the user const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -71,9 +51,7 @@ export default define(meta, paramDef, async (ps, me) => { userId: user.id, }); - if (joining == null) { - throw new ApiError(meta.errors.noSuchGroupMember); - } + if (joining == null) throw new ApiError('NO_SUCH_USER', 'The user exists but is not a member of the group.'); await UserGroups.update(userGroup.id, { userId: ps.userId, diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts index 0a96165fc..81d5f26ec 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/update.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/update.ts @@ -17,13 +17,7 @@ export const meta = { ref: 'UserGroup', }, - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6', - }, - }, + errors: ['NO_SUCH_GROUP'], } as const; export const paramDef = { @@ -43,9 +37,7 @@ export default define(meta, paramDef, async (ps, me) => { userId: me.id, }); - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } + if (userGroup == null) throw new ApiError('NO_SUCH_GROUP'); await UserGroups.update(userGroup.id, { name: ps.name, diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index 5a7613c98..e8f476794 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -11,13 +11,7 @@ export const meta = { description: 'Delete an existing list of users.', - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '78436795-db79-42f5-b1e2-55ea2cf19166', - }, - }, + errors: ['NO_SUCH_USER_LIST'], } as const; export const paramDef = { @@ -35,9 +29,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } + if (userList == null) throw new ApiError('NO_SUCH_USER_LIST'); await UserLists.delete(userList.id); }); diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index d3d1d6555..0860b97ef 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -13,19 +13,7 @@ export const meta = { description: 'Remove a user from a list.', - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '588e7f72-c744-4a61-b180-d354e912bda2', - }, - }, + errors: ['NO_SUCH_USER', 'NO_SUCH_USER_LIST'], } as const; export const paramDef = { @@ -45,13 +33,11 @@ export default define(meta, paramDef, async (ps, me) => { userId: me.id, }); - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } + if (userList == null) throw new ApiError('NO_SUCH_USER_LIST'); // Fetch the user const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 12b7b8634..7552f3331 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -13,31 +13,7 @@ export const meta = { description: 'Add a user to an existing list.', - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '2214501d-ac96-4049-b717-91e42272a711', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'a89abd3d-f0bc-4cce-beb1-2f446f4f1e6a', - }, - - alreadyAdded: { - message: 'That user has already been added to that list.', - code: 'ALREADY_ADDED', - id: '1de7c884-1595-49e9-857e-61f12f4d4fc5', - }, - - youHaveBeenBlocked: { - message: 'You cannot push this user because you have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b', - }, - }, + errors: ['ALREADY_ADDED', 'BLOCKED', 'NO_SUCH_USER', 'NO_SUCH_USER_LIST'], } as const; export const paramDef = { @@ -57,13 +33,11 @@ export default define(meta, paramDef, async (ps, me) => { userId: me.id, }); - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } + if (userList == null) throw new ApiError('NO_SUCH_USER_LIST'); // Fetch the user const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -73,9 +47,7 @@ export default define(meta, paramDef, async (ps, me) => { blockerId: user.id, blockeeId: me.id, }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } + if (block) throw new ApiError('BLOCKED'); } const exist = await UserListJoinings.findOneBy({ @@ -83,9 +55,7 @@ export default define(meta, paramDef, async (ps, me) => { userId: user.id, }); - if (exist) { - throw new ApiError(meta.errors.alreadyAdded); - } + if (exist) throw new ApiError('ALREADY_ADDED'); // Push the user await pushUserToUserList(user, userList); diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index fd0612f73..64f280a61 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -17,13 +17,7 @@ export const meta = { ref: 'UserList', }, - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', - }, - }, + errors: ['NO_SUCH_USER_LIST'], } as const; export const paramDef = { @@ -42,9 +36,7 @@ export default define(meta, paramDef, async (ps, me) => { userId: me.id, }); - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } + if (userList == null) throw new ApiError('NO_SUCH_USER_LIST'); return await UserLists.pack(userList); }); diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index 65e708b95..f0df2d282 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -17,13 +17,7 @@ export const meta = { ref: 'UserList', }, - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '796666fe-3dff-4d39-becb-8a5932c1d5b7', - }, - }, + errors: ['NO_SUCH_USER_LIST'], } as const; export const paramDef = { @@ -43,9 +37,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, }); - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } + if (userList == null) throw new ApiError('NO_SUCH_USER_LIST'); await UserLists.update(userList.id, { name: ps.name, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 9fa56fe83..f327e07a5 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -23,13 +23,7 @@ export const meta = { }, }, - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b', - }, - }, + errors: ['NO_SUCH_USER'], } as const; export const paramDef = { @@ -56,7 +50,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { // Lookup user const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); @@ -77,7 +71,7 @@ export default define(meta, paramDef, async (ps, me) => { generateVisibilityQuery(query, me); if (me) { - generateMutedUserQuery(query, me, user); + generateMutedUserQuery(query, me); generateBlockedUserQuery(query, me); } diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 4750dc4f9..67ef1271f 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -21,13 +21,7 @@ export const meta = { }, }, - errors: { - reactionsNotPublic: { - message: 'Reactions of the user is not public.', - code: 'REACTIONS_NOT_PUBLIC', - id: '673a7dd2-6924-1093-e0c0-e68456ceae5c', - }, - }, + errors: ['ACCESS_DENIED'], } as const; export const paramDef = { @@ -48,7 +42,7 @@ export default define(meta, paramDef, async (ps, me) => { const profile = await UserProfiles.findOneByOrFail({ userId: ps.userId }); if (me == null || (me.id !== ps.userId && !profile.publicReactions)) { - throw new ApiError(meta.errors.reactionsNotPublic); + throw new ApiError('ACCESS_DENIED'); } const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'), diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index eacfe72f4..7ad92293f 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -16,25 +16,7 @@ export const meta = { description: 'File a report.', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '1acefcb5-0959-43fd-9685-b48305736cb5', - }, - - cannotReportYourself: { - message: 'Cannot report yourself.', - code: 'CANNOT_REPORT_YOURSELF', - id: '1e13149e-b1e8-43cf-902e-c01dbfcb202f', - }, - - cannotReportAdmin: { - message: 'Cannot report the admin.', - code: 'CANNOT_REPORT_THE_ADMIN', - id: '35e166f5-05fb-4f87-a2d5-adb42676d48f', - }, - }, + errors: ['NO_SUCH_USER', 'CANNOT_REPORT_ADMIN', 'CANNOT_REPORT_YOURSELF'], } as const; export const paramDef = { @@ -51,17 +33,13 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { // Lookup user const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); throw e; }); - if (user.id === me.id) { - throw new ApiError(meta.errors.cannotReportYourself); - } + if (user.id === me.id) throw new ApiError('CANNOT_REPORT_YOURSELF'); - if (user.isAdmin) { - throw new ApiError(meta.errors.cannotReportAdmin); - } + if (user.isAdmin) throw new ApiError('CANNOT_REPORT_ADMIN'); const uri = user.host == null ? `${config.url}/users/${user.id}` : user.uri; if (!ps.urls.includes(uri)) { diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 846d83b49..33c0cfca3 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -30,20 +30,7 @@ export const meta = { ], }, - errors: { - failedToResolveRemoteUser: { - message: 'Failed to resolve remote user.', - code: 'FAILED_TO_RESOLVE_REMOTE_USER', - id: 'ef7b9be4-9cba-4e6f-ab41-90ed171c7d3c', - kind: 'server', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '4362f8dc-731f-4ad8-a694-be5a88922a24', - }, - }, + errors: ['FAILED_TO_RESOLVE_REMOTE_USER', 'NO_SUCH_USER'], } as const; export const paramDef = { @@ -109,7 +96,7 @@ export default define(meta, paramDef, async (ps, me) => { if (typeof ps.host === 'string' && typeof ps.username === 'string') { user = await resolveUser(ps.username, ps.host).catch(e => { apiLogger.warn(`failed to resolve remote user: ${e}`); - throw new ApiError(meta.errors.failedToResolveRemoteUser); + throw new ApiError('FAILED_TO_RESOLVE_REMOTE_USER'); }); } else { const q: FindOptionsWhere = ps.userId != null @@ -120,7 +107,7 @@ export default define(meta, paramDef, async (ps, me) => { } if (user == null || (!isAdminOrModerator && user.isSuspended)) { - throw new ApiError(meta.errors.noSuchUser); + throw new ApiError('NO_SUCH_USER'); } return await Users.pack(user, me, { diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts index 47f322ee9..55d4f4314 100644 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -10,13 +10,7 @@ export const meta = { description: 'Show statistics about a user.', - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '9e638e45-3b25-4ef7-8f95-07e8498f1819', - }, - }, + errors: ['NO_SUCH_USER'], res: { type: 'object', @@ -119,7 +113,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const user = await Users.findOneBy({ id: ps.userId }); if (user == null) { - throw new ApiError(meta.errors.noSuchUser); + throw new ApiError('NO_SUCH_USER'); } const result = await awaitAll({ diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index 6fc5ea38b..9323f8919 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -1,29 +1,358 @@ -type E = { message: string, code: string, id: string, kind?: 'client' | 'server', httpStatusCode?: number }; - export class ApiError extends Error { public message: string; public code: string; - public id: string; - public kind: string; - public httpStatusCode?: number; + public httpStatusCode: number; public info?: any; constructor( - e: E = { - message: 'Internal error occurred. Please contact us if the error persists.', - code: 'INTERNAL_ERROR', - id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac', - kind: 'server', - httpStatusCode: 500, - }, + code: keyof errors = 'INTERNAL_ERROR', info?: any | null, ) { - super(e.message); - this.message = e.message; - this.code = e.code; - this.id = e.id; - this.kind = e.kind ?? 'client'; - this.httpStatusCode = e.httpStatusCode ?? 500; + if (!(code in errors)) { + info = `Unknown error "${code}" occurred.`; + code = 'INTERNAL_ERROR'; + } + + const { message, httpStatusCode } = errors[code]; + super(message); + this.code = code; this.info = info; + this.message = message; + this.httpStatusCode = httpStatusCode; } } + +export const errors: Record = { + ACCESS_DENIED: { + message: 'Access denied.', + httpStatusCode: 403, + }, + ALREADY_ADDED: { + message: 'That user has already been added to that list or group.', + httpStatusCode: 409, + }, + ALREADY_BLOCKING: { + message: 'You are already blocking that user.', + httpStatusCode: 409, + }, + ALREADY_CLIPPED: { + message: 'That note is already added to that clip.', + httpStatusCode: 409, + }, + ALREADY_FAVORITED: { + message: 'That note is already favorited.', + httpStatusCode: 409, + }, + ALREADY_FOLLOWING: { + message: 'You are already following that user.', + httpStatusCode: 409, + }, + ALREADY_INVITED: { + message: 'That user has already been invited to that group.', + httpStatusCode: 409, + }, + ALREADY_LIKED: { + message: 'You already liked that page or gallery post.', + httpStatusCode: 409, + }, + ALREADY_MUTING: { + message: 'You are already muting that user.', + httpStatusCode: 409, + }, + ALREADY_PINNED: { + message: 'You already pinned that note.', + httpStatusCode: 409, + }, + ALREADY_REACTED: { + message: 'You already reacted to that note.', + httpStatusCode: 409, + }, + ALREADY_VOTED: { + message: 'You have already voted in that poll.', + httpStatusCode: 409, + }, + AUTHENTICATION_FAILED: { + message: 'Authentication failed.', + httpStatusCode: 401, + }, + AUTHENTICATION_REQUIRED: { + message: 'Authentication is required, but authenticating information was not or not appropriately provided.', + httpStatusCode: 401, + }, + BLOCKED: { + message: 'You are blocked by that user.', + httpStatusCode: 400, + }, + BLOCKEE_IS_YOURSELF: { + message: 'You cannot block yourself.', + httpStatusCode: 400, + }, + BLOCKING: { + message: 'You are blocking that user.', + httpStatusCode: 400, + }, + CANNOT_REPORT_ADMIN: { + message: 'You cannot report an administrator.', + httpStatusCode: 400, + }, + CANNOT_REPORT_YOURSELF: { + message: 'You cannot report yourself.', + httpStatusCode: 400, + }, + EMPTY_FILE: { + message: 'The provided file is empty.', + httpStatusCode: 400, + }, + EXPIRED_POLL: { + message: 'Poll is already expired.', + httpStatusCode: 400, + }, + FAILED_TO_RESOLVE_REMOTE_USER: { + message: 'Failed to resolve remote user.', + httpStatusCode: 502, + }, + FILE_TOO_BIG: { + message: 'The provided file is too big.', + httpStatusCode: 400, + }, + FILE_REQUIRED: { + message: 'This operation requires a file to be provided.', + httpStatusCode: 400, + }, + FOLLOWEE_IS_YOURSELF: { + message: 'You cannot follow yourself.', + httpStatusCode: 400, + }, + FOLLOWER_IS_YOURSELF: { + message: 'You cannot unfollow yourself.', + httpStatusCode: 400, + }, + GROUP_OWNER: { + message: 'The owner of a group may not leave. Instead, ownership can be transferred or the group deleted.', + httpStatusCode: 400, + }, + HAS_CHILD_FILES_OR_FOLDERS: { + message: 'That folder is not empty.', + httpStatusCode: 400, + }, + INTERNAL_ERROR: { + message: 'Internal error occurred. Please contact us if the error persists.', + httpStatusCode: 500, + }, + INVALID_CHOICE: { + message: 'Choice index is invalid.', + httpStatusCode: 400, + }, + INVALID_FILE_NAME: { + message: 'Invalid file name.', + httpStatusCode: 400, + }, + INVALID_PARAM: { + message: 'One or more parameters do not match the API definition.', + httpStatusCode: 400, + }, + INVALID_PASSWORD: { + message: 'The provided password is not suitable.', + httpStatusCode: 400, + }, + INVALID_REGEXP: { + message: 'Invalid Regular Expression', + httpStatusCode: 400, + }, + INVALID_URL: { + message: 'Invalid URL.', + httpStatusCode: 400, + }, + INVALID_USERNAME: { + message: 'Invalid username.', + httpStatusCode: 400, + }, + LESS_RESTRICTIVE_VISIBILITY: { + message: 'The visibility cannot be less restrictive than the parent note.', + httpStatusCode: 400, + }, + MUTEE_IS_YOURSELF: { + message: 'You cannot mute yourself.', + httpStatusCode: 400, + }, + NAME_ALREADY_EXISTS: { + message: 'The specified name already exists.', + httpStatusCode: 409, + }, + NO_POLL: { + message: 'The note does not have an attached poll.', + httpStatusCode: 404, + }, + NO_SUCH_ANNOUNCEMENT: { + message: 'No such announcement.', + httpStatusCode: 404, + }, + NO_SUCH_ANTENNA: { + message: 'No such antenna.', + httpStatusCode: 404, + }, + NO_SUCH_APP: { + message: 'No such app.', + httpStatusCode: 404, + }, + NO_SUCH_CLIP: { + message: 'No such clip.', + httpStatusCode: 404, + }, + NO_SUCH_CHANNEL: { + message: 'No such channel.', + httpStatusCode: 404, + }, + NO_SUCH_EMOJI: { + message: 'No such emoji.', + httpStatusCode: 404, + }, + NO_SUCH_ENDPOINT: { + message: 'No such endpoint.', + httpStatusCode: 404, + }, + NO_SUCH_FILE: { + message: 'No such file.', + httpStatusCode: 404, + }, + NO_SUCH_FOLDER: { + message: 'No such folder.', + httpStatusCode: 404, + }, + NO_SUCH_FOLLOW_REQUEST: { + message: 'No such follow request.', + httpStatusCode: 404, + }, + NO_SUCH_GROUP: { + message: 'No such user group.', + httpStatusCode: 404, + }, + NO_SUCH_HASHTAG: { + message: 'No such hashtag.', + httpStatusCode: 404, + }, + NO_SUCH_INVITATION: { + message: 'No such group invitation.', + httpStatusCode: 404, + }, + NO_SUCH_KEY: { + message: 'No such key.', + httpStatusCode: 404, + }, + NO_SUCH_NOTE: { + message: 'No such note.', + httpStatusCode: 404, + }, + NO_SUCH_NOTIFICATION: { + message: 'No such notification.', + httpStatusCode: 404, + }, + NO_SUCH_MESSAGE: { + message: 'No such message.', + httpStatusCode: 404, + }, + NO_SUCH_OBJECT: { + message: 'No such object.', + httpStatusCode: 404, + }, + NO_SUCH_PAGE: { + message: 'No such page.', + httpStatusCode: 404, + }, + NO_SUCH_PARENT_FOLDER: { + 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, + }, + NO_SUCH_SESSION: { + message: 'No such session', + httpStatusCode: 404, + }, + NO_SUCH_USER: { + message: 'No such user.', + httpStatusCode: 404, + }, + NO_SUCH_USER_LIST: { + message: 'No such user list.', + httpStatusCode: 404, + }, + NO_SUCH_WEBHOOK: { + message: 'No such webhook.', + httpStatusCode: 404, + }, + NOT_AN_IMAGE: { + message: 'The file specified was expected to be an image, but it is not.', + httpStatusCode: 400, + }, + NOT_BLOCKING: { + message: 'You are not blocking that user.', + httpStatusCode: 409, + }, + NOT_CLIPPED: { + message: 'That note is not added to that clip.', + httpStatusCode: 409, + }, + NOT_FAVORITED: { + message: 'You have not favorited that note.', + httpStatusCode: 409, + }, + NOT_FOLLOWING: { + message: 'You are not following that user.', + httpStatusCode: 409, + }, + NOT_LIKED: { + message: 'You have not liked that page or gallery post.', + httpStatusCode: 409, + }, + NOT_MUTING: { + message: 'You are not muting that user.', + httpStatusCode: 409, + }, + NOT_REACTED: { + message: 'You have not reacted to that note.', + httpStatusCode: 409, + }, + PENDING_SESSION: { + message: 'That authorization process has not been completed yet.', + httpStatusCode: 400, + }, + PIN_LIMIT_EXCEEDED: { + message: 'You can not pin any more notes.', + httpStatusCode: 400, + }, + PURE_RENOTE: { + message: 'You cannot renote or reply to a pure renote.', + httpStatusCode: 400, + }, + RATE_LIMIT_EXCEEDED: { + message: 'Rate limit exceeded. Please try again later.', + httpStatusCode: 429, + }, + RECIPIENT_IS_YOURSELF: { + message: 'You cannot send a message to yourself.', + httpStatusCode: 400, + }, + RECURSIVE_FOLDER: { + message: 'Folder cannot be its own parent.', + httpStatusCode: 400, + }, + SUSPENDED: { + message: 'Your account has been suspended.', + httpStatusCode: 403, + }, + TIMELINE_DISABLED: { + message: 'This timeline is disabled by an administrator.', + httpStatusCode: 503, + }, + USED_USERNAME: { + message: 'That username is not available because it is being used or has been used before. Usernames cannot be reassigned.', + httpStatusCode: 409, + }, +}; diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index 83ece51f5..140649dcc 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -11,7 +11,7 @@ import cors from '@koa/cors'; import { Instances, AccessTokens, Users } from '@/models/index.js'; import config from '@/config/index.js'; import endpoints from './endpoints.js'; -import handler from './api-handler.js'; +import { handler } from './api-handler.js'; import signup from './private/signup.js'; import signin from './private/signin.js'; import signupPending from './private/signup-pending.js'; @@ -32,10 +32,7 @@ app.use(async (ctx, next) => { await next(); }); -app.use(bodyParser({ - // リクエストが multipart/form-data でない限りはJSONだと見なす - detectJSON: ctx => !ctx.is('multipart/form-data'), -})); +app.use(bodyParser()); // Init multer instance const upload = multer({ diff --git a/packages/backend/src/server/api/openapi/errors.ts b/packages/backend/src/server/api/openapi/errors.ts deleted file mode 100644 index 3f733b4ea..000000000 --- a/packages/backend/src/server/api/openapi/errors.ts +++ /dev/null @@ -1,69 +0,0 @@ - -export const errors = { - '400': { - 'INVALID_PARAM': { - value: { - error: { - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '3d81ceae-475f-4600-b2a8-2bc116157532', - }, - }, - }, - }, - '401': { - 'CREDENTIAL_REQUIRED': { - value: { - error: { - message: 'Credential required.', - code: 'CREDENTIAL_REQUIRED', - id: '1384574d-a912-4b81-8601-c7b1c4085df1', - }, - }, - }, - }, - '403': { - 'AUTHENTICATION_FAILED': { - value: { - error: { - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - }, - }, - }, - }, - '418': { - 'I_AM_A_TEAPOT': { - value: { - error: { - message: 'I am a teapot.', - code: 'I_AM_A_TEAPOT', - id: '60c46cd1-f23a-46b1-bebe-5d2b73951a84', - }, - }, - }, - }, - '429': { - 'RATE_LIMIT_EXCEEDED': { - value: { - error: { - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - }, - }, - }, - }, - '500': { - 'INTERNAL_ERROR': { - value: { - error: { - message: 'Internal error occurred. Please contact us if the error persists.', - code: 'INTERNAL_ERROR', - id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac', - }, - }, - }, - }, -}; diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index e205ab51e..f9795884d 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -1,7 +1,8 @@ import config from '@/config/index.js'; +import { errors as errorDefinitions } from '../error.js'; import endpoints from '../endpoints.js'; -import { errors as basicErrors } from './errors.js'; import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; +import { httpCodes } from './http-codes.js'; export function genOpenapiSpec() { const spec = { @@ -43,19 +44,75 @@ export function genOpenapiSpec() { }; for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { - const errors = {} as any; + // generate possible responses, first starting with errors + const responses = [ + // general error codes that can always happen + 'INVALID_PARAM', + 'INTERNAL_ERROR', + // error codes that happen only if authentication is required + ...(!endpoint.meta.requireCredential ? [] : [ + 'ACCESS_DENIED', + 'AUTHENTICATION_REQUIRED', + 'AUTHENTICATION_FAILED', + 'SUSPENDED', + ]), + // error codes that happen only if a rate limit is defined + ...(!endpoint.meta.limit ? [] : [ + 'RATE_LIMIT_EXCEEDED', + ]), + // error codes that happen only if a file is required + ...(!endpoint.meta.requireFile ? [] : [ + 'FILE_REQUIRED', + ]), + // endpoint specific error codes + ...(endpoint.meta.errors ?? []), + ] + .reduce((acc, code) => { + const { message, httpStatusCode } = errorDefinitions[code]; + const httpCode = httpStatusCode.toString(); - if (endpoint.meta.errors) { - for (const e of Object.values(endpoint.meta.errors)) { - errors[e.code] = { - value: { - error: e, + if (!(httpCode in acc)) { + acc[httpCode] = { + description: httpCodes[httpCode], + content: { + 'application/json': { + schema: { + '$ref': '#/components/schemas/Error', + }, + examples: {}, + }, }, }; } - } - const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; + acc[httpCode].content['application/json'].examples[code] = { + value: { + error: { + code, + message, + endpoint: endpoint.name, + }, + }, + }; + + return acc; + }, {}); + + // add successful response + if (endpoint.meta.res) { + responses['200'] = { + description: 'OK', + content: { + 'application/json': { + schema: convertSchemaToOpenApiSchema(endpoint.meta.res), + }, + }, + }; + } else { + responses['204'] = { + description: 'No Content', + }; + } let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; @@ -107,90 +164,7 @@ export function genOpenapiSpec() { }, }, }, - responses: { - ...(endpoint.meta.res ? { - '200': { - description: 'OK (with results)', - content: { - 'application/json': { - schema: resSchema, - }, - }, - }, - } : { - '204': { - description: 'OK (without any results)', - }, - }), - '400': { - description: 'Client error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: { ...errors, ...basicErrors['400'] }, - }, - }, - }, - '401': { - description: 'Authentication error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['401'], - }, - }, - }, - '403': { - description: 'Forbidden error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['403'], - }, - }, - }, - '418': { - description: 'I\'m Ai', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['418'], - }, - }, - }, - ...(endpoint.meta.limit ? { - '429': { - description: 'To many requests', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['429'], - }, - }, - }, - } : {}), - '500': { - description: 'Internal server error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['500'], - }, - }, - }, - }, + responses, }; const path = { @@ -200,6 +174,7 @@ export function genOpenapiSpec() { path.get = { ...info }; // API Key authentication is not permitted for GET requests path.get.security = path.get.security.filter(elem => !Object.prototype.hasOwnProperty.call(elem, 'ApiKeyAuth')); + // fix the way parameters are passed delete path.get.requestBody; path.get.parameters = []; diff --git a/packages/backend/src/server/api/openapi/http-codes.ts b/packages/backend/src/server/api/openapi/http-codes.ts new file mode 100644 index 000000000..d08b10673 --- /dev/null +++ b/packages/backend/src/server/api/openapi/http-codes.ts @@ -0,0 +1,67 @@ +export const httpCodes: Record = { + '100': 'Continue', + '101': 'Switching Protocols', + '102': 'Processing', + '103': 'Early Hints', + '200': 'OK', + '201': 'Created', + '202': 'Accepted', + '203': 'Non-Authoritative Information', + '204': 'No Content', + '205': 'Reset Content', + '206': 'Partial Content', + '207': 'Multi-Status', + '208': 'Already Reported', + '226': 'IM Used', + '300': 'Multiple Choices', + '301': 'Moved Permanently', + '302': 'Found', + '303': 'See Other', + '304': 'Not Modified', + '305': 'Use Proxy', + '307': 'Temporary Redirect', + '308': 'Permanent Redirect', + '400': 'Bad Request', + '401': 'Unauthorized', + '402': 'Payment Required', + '403': 'Forbidden', + '404': 'Not Found', + '405': 'Method Not Allowed', + '406': 'Not Acceptable', + '407': 'Proxy Authentication Required', + '408': 'Request Timeout', + '409': 'Conflict', + '410': 'Gone', + '411': 'Length Required', + '412': 'Precondition Failed', + '413': 'Content Too Large', + '414': 'URI Too Long', + '415': 'Unsupported Media Type', + '416': 'Range Not Satisfiable', + '417': 'Expectation Failed', + '418': 'I\'m a Teapot', + '421': 'Misdirected Request', + '422': 'Unprocessable Content', + '423': 'Locked', + '424': 'Failed Dependency', + '425': 'Too Early', + '426': 'Upgrade Required', + '427': 'Unassigned', + '428': 'Precondition Required', + '429': 'Too Many Requests', + '430': 'Unassigned', + '431': 'Request Header Fields Too Large', + '451': 'Unavailable For Legal Reasons', + '500': 'Internal Server Error', + '501': 'Not Implemented', + '502': 'Bad Gateway', + '503': 'Service Unavailable', + '504': 'Gateway Timeout', + '505': 'HTTP Version Not Supported', + '506': 'Variant Also Negotiates', + '507': 'Insufficient Storage', + '508': 'Loop Detected', + '509': 'Unassigned', + '510': 'Not Extended', + '511': 'Network Authentication Required', +}; diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index 4a0844b42..04346c53b 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -36,19 +36,21 @@ export const schemas = { properties: { code: { type: 'string', - description: 'An error code. Unique within the endpoint.', + description: 'A machine and human readable error code.', + }, + endpoint: { + type: 'string', + description: 'Name of the API endpoint the error happened in.', }, message: { type: 'string', - description: 'An error message.', + description: 'A human readable error description in English.', }, - id: { - type: 'string', - format: 'uuid', - description: 'An error ID. This ID is static.', + info: { + description: 'Potentially more information, primarily intended for developers.', }, }, - required: ['code', 'id', 'message'], + required: ['code', 'endpoint', 'message'], }, }, required: ['error'], diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index 667d721ad..c9ce555e7 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -11,48 +11,51 @@ import { getIpHash } from '@/misc/get-ip-hash.js'; import signin from '../common/signin.js'; import { verifyLogin, hash } from '../2fa.js'; import { limiter } from '../limiter.js'; +import { ApiError } from '../error.js'; export default async (ctx: Koa.Context) => { ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Credentials', 'true'); const body = ctx.request.body as any; - const username = body['username']; - const password = body['password']; - const token = body['token']; + const { username, password, token } = body; - function error(status: number, error: { id: string }) { - ctx.status = status; - ctx.body = { error }; + // taken from @server/api/api-handler.ts + function error (e: ApiError): void { + ctx.status = e.httpStatusCode; + if (e.httpStatusCode === 401) { + ctx.response.set('WWW-Authenticate', 'Bearer'); + } + ctx.body = { + error: { + message: e!.message, + code: e!.code, + ...(e!.info ? { info: e!.info } : {}), + endpoint: endpoint.name, + }, + }; } try { // not more than 1 attempt per second and not more than 10 attempts per hour await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip)); } catch (err) { - ctx.status = 429; - ctx.body = { - error: { - message: 'Too many failed attempts to sign in. Try again later.', - code: 'TOO_MANY_AUTHENTICATION_FAILURES', - id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', - }, - }; + error(new ApiError('RATE_LIMIT_EXCEEDED')); return; } if (typeof username !== 'string') { - ctx.status = 400; + error(new ApiError('INVALID_PARAM', { param: 'username', reason: 'not a string' })); return; } if (typeof password !== 'string') { - ctx.status = 400; + error(new ApiError('INVALID_PARAM', { param: 'password', reason: 'not a string' })); return; } if (token != null && typeof token !== 'string') { - ctx.status = 400; + error(new ApiError('INVALID_PARAM', { param: 'token', reason: 'provided but not a string' })); return; } @@ -63,16 +66,12 @@ export default async (ctx: Koa.Context) => { }) as ILocalUser; if (user == null) { - error(404, { - id: '6cc579cc-885d-43d8-95c2-b8c7fc963280', - }); + error(new ApiError('NO_SUCH_USER')); return; } if (user.isSuspended) { - error(403, { - id: 'e03a5f46-d309-4865-9b69-56282d94e1eb', - }); + error(new ApiError('SUSPENDED')); return; } @@ -81,7 +80,7 @@ export default async (ctx: Koa.Context) => { // Compare password const same = await bcrypt.compare(password, profile.password!); - async function fail(status?: number, failure?: { id: string }) { + async function fail(): void { // Append signin history await Signins.insert({ id: genId(), @@ -92,7 +91,7 @@ export default async (ctx: Koa.Context) => { success: false, }); - error(status || 500, failure || { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); + error(new ApiError('ACCESS_DENIED')); } if (!profile.twoFactorEnabled) { @@ -100,18 +99,14 @@ export default async (ctx: Koa.Context) => { signin(ctx, user); return; } else { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', - }); + await fail(); return; } } if (token) { if (!same) { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', - }); + await fail(); return; } @@ -126,16 +121,12 @@ export default async (ctx: Koa.Context) => { signin(ctx, user); return; } else { - await fail(403, { - id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f', - }); + await fail(); return; } } else if (body.credentialId) { if (!same && !profile.usePasswordLessLogin) { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', - }); + await fail(); return; } @@ -149,9 +140,7 @@ export default async (ctx: Koa.Context) => { }); if (!challenge) { - await fail(403, { - id: '2715a88a-2125-4013-932f-aa6fe72792da', - }); + await fail(); return; } @@ -161,9 +150,7 @@ export default async (ctx: Koa.Context) => { }); if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) { - await fail(403, { - id: '2715a88a-2125-4013-932f-aa6fe72792da', - }); + await fail(); return; } @@ -177,9 +164,7 @@ export default async (ctx: Koa.Context) => { }); if (!securityKey) { - await fail(403, { - id: '66269679-aeaf-4474-862b-eb761197e046', - }); + await fail(); return; } @@ -196,16 +181,12 @@ export default async (ctx: Koa.Context) => { signin(ctx, user); return; } else { - await fail(403, { - id: '93b86c4b-72f9-40eb-9815-798928603d1e', - }); + await fail(); return; } } else { if (!same && !profile.usePasswordLessLogin) { - await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', - }); + await fail(); return; } @@ -214,9 +195,7 @@ export default async (ctx: Koa.Context) => { }); if (keys.length === 0) { - await fail(403, { - id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4', - }); + await fail(); return; } diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index ba88685a1..a42042d13 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -62,21 +62,21 @@ export default abstract class Channel { }); } - protected withPackedNote(callback: (note: Packed<'Note'>) => void): (Note) => void { + protected withPackedNote(callback: (note: Packed<'Note'>) => Promise): (note: Note) => Promise { return async (note: Note) => { try { // because `note` was previously JSON.stringify'ed, the fields that // were objects before are now strings and have to be restored or // removed from the object note.createdAt = new Date(note.createdAt); - delete note.reply; - delete note.renote; - delete note.user; - delete note.channel; + note.reply = null; + note.renote = null; + note.user = null; + note.channel = null; const packed = await Notes.pack(note, this.user, { detail: true }); - callback(packed); + await callback(packed); } catch (err) { if (err instanceof IdentifiableError && err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') { // skip: note not visible to user diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 727bfbd1e..f16a9f670 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -2,6 +2,7 @@ import { Users } from '@/models/index.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { User } from '@/models/entities/user.js'; import { Packed } from '@/misc/schema.js'; +import { Note } from '@/models/entities/note.js'; import { StreamMessages } from '../types.js'; import Channel from '../channel.js'; @@ -12,10 +13,11 @@ export default class extends Channel { private channelId: string; private typers: Record = {}; private emitTypersIntervalId: ReturnType; + private onNote: (note: Note) => Promise; constructor(id: string, connection: Channel['connection']) { super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); + this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } public async init(params: any) { @@ -27,7 +29,7 @@ export default class extends Channel { this.emitTypersIntervalId = setInterval(this.emitTypers, 5000); } - private async onNote(note: Packed<'Note'>) { + private async onPackedNote(note: Packed<'Note'>): Promise { if (note.channelId !== this.channelId) return; // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 1087d4b91..6b6f69dfe 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -3,16 +3,18 @@ import { checkWordMute } from '@/misc/check-word-mute.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { Packed } from '@/misc/schema.js'; +import { Note } from '@/models/entities/note.js'; import Channel from '../channel.js'; export default class extends Channel { public readonly chName = 'globalTimeline'; public static shouldShare = true; public static requireCredential = false; + private onNote: (note: Note) => Promise; constructor(id: string, connection: Channel['connection']) { super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); + this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } public async init(params: any) { @@ -25,7 +27,7 @@ export default class extends Channel { this.subscriber.on('notesStream', this.onNote); } - private async onNote(note: Packed<'Note'>) { + private async onPackedNote(note: Packed<'Note'>): Promise { if (note.visibility !== 'public') return; if (note.channelId != null) return; diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 9ec7e1962..3a4c4ad22 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -1,6 +1,7 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { Packed } from '@/misc/schema.js'; +import { Note } from '@/models/entities/note.js'; import Channel from '../channel.js'; export default class extends Channel { @@ -8,10 +9,11 @@ export default class extends Channel { public static shouldShare = false; public static requireCredential = false; private q: string[][]; + private onNote: (note: Note) => Promise; constructor(id: string, connection: Channel['connection']) { super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); + this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } public async init(params: any) { @@ -23,7 +25,7 @@ export default class extends Channel { this.subscriber.on('notesStream', this.onNote); } - private async onNote(note: Packed<'Note'>) { + private async onPackedNote(note: Packed<'Note'>): Promise { const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : []; const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag)))); if (!matched) return; diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index a7965491a..e4a5bfd43 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -2,16 +2,18 @@ import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { Packed } from '@/misc/schema.js'; +import { Note } from '@/models/entities/note.js'; import Channel from '../channel.js'; export default class extends Channel { public readonly chName = 'homeTimeline'; public static shouldShare = true; public static requireCredential = true; + private onNote: (note: Note) => Promise; constructor(id: string, connection: Channel['connection']) { super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); + this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } public async init(params: any) { @@ -19,7 +21,7 @@ export default class extends Channel { this.subscriber.on('notesStream', this.onNote); } - private async onNote(note: Packed<'Note'>) { + private async onPackedNote(note: Packed<'Note'>): Promise { if (note.channelId) { if (!this.followingChannels.has(note.channelId)) return; } else { diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index d17a24c70..989e70590 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -3,16 +3,18 @@ import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { Packed } from '@/misc/schema.js'; +import { Note } from '@/models/entities/note.js'; import Channel from '../channel.js'; export default class extends Channel { public readonly chName = 'hybridTimeline'; public static shouldShare = true; public static requireCredential = true; + private onNote: (note: Note) => Promise; constructor(id: string, connection: Channel['connection']) { super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); + this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } public async init(params: any) { @@ -23,7 +25,7 @@ export default class extends Channel { this.subscriber.on('notesStream', this.onNote); } - private async onNote(note: Packed<'Note'>) { + private async onPackedNote(note: Packed<'Note'>): Promise { // チャンネルの投稿ではなく、自分自身の投稿 または // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または // チャンネルの投稿ではなく、全体公開のローカルの投稿 または diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 987ed2a32..3aa76e389 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -2,16 +2,18 @@ import { fetchMeta } from '@/misc/fetch-meta.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { Packed } from '@/misc/schema.js'; +import { Note } from '@/models/entities/note.js'; import Channel from '../channel.js'; export default class extends Channel { public readonly chName = 'localTimeline'; public static shouldShare = true; public static requireCredential = false; + onNote: (note: Note) => Promise; constructor(id: string, connection: Channel['connection']) { super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); + this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } public async init(params: any) { @@ -24,7 +26,7 @@ export default class extends Channel { this.subscriber.on('notesStream', this.onNote); } - private async onNote(note: Packed<'Note'>) { + private async onPackedNote(note: Packed<'Note'>): Promise { if (note.user.host !== null) return; if (note.visibility !== 'public') return; if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 16690a368..39ee53c2b 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -2,6 +2,7 @@ import { UserListJoinings, UserLists } from '@/models/index.js'; import { User } from '@/models/entities/user.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { Packed } from '@/misc/schema.js'; +import { Note } from '@/models/entities/note.js'; import Channel from '../channel.js'; export default class extends Channel { @@ -11,11 +12,12 @@ export default class extends Channel { private listId: string; public listUsers: User['id'][] = []; private listUsersClock: NodeJS.Timer; + private onNote: (note: Note) => Promise; constructor(id: string, connection: Channel['connection']) { super(id, connection); this.updateListUsers = this.updateListUsers.bind(this); - this.onNote = this.withPackedNote(this.onNote.bind(this)); + this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } public async init(params: any) { @@ -48,7 +50,7 @@ export default class extends Channel { this.listUsers = users.map(x => x.userId); } - private async onNote(note: Packed<'Note'>) { + private async onPackedNote(note: Packed<'Note'>): Promise { if (!this.listUsers.includes(note.userId)) return; // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index be67aa226..f3337fbfe 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events'; import * as websocket from 'websocket'; -import readNote from '@/services/note/read.js'; +import { readNote } from '@/services/note/read.js'; import { User } from '@/models/entities/user.js'; import { Channel as ChannelModel } from '@/models/entities/channel.js'; import { Followings, Mutings, RenoteMutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js'; diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index e27ce34af..6fab792ae 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -1,5 +1,4 @@ import { EventEmitter } from 'events'; -import Emitter from 'strict-event-emitter-types'; import { Channel } from '@/models/entities/channel.js'; import { User } from '@/models/entities/user.js'; import { UserProfile } from '@/models/entities/user-profile.js'; @@ -15,6 +14,7 @@ import { Signin } from '@/models/entities/signin.js'; import { Page } from '@/models/entities/page.js'; import { Packed } from '@/misc/schema.js'; import { Webhook } from '@/models/entities/webhook.js'; +import type { StrictEventEmitter as Emitter } from 'strict-event-emitter-types'; //#region Stream type-body definitions export interface InternalStreamTypes { diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index 4d3ae0043..9a897bc6d 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -138,13 +138,13 @@ export const startServer = () => { return server; }; -export default () => new Promise(resolve => { +export default (): Promise => new Promise(resolve => { const server = createServer(); initializeStreamingServer(server); server.on('error', e => { - switch ((e as any).code) { + switch ((e as NodeJS.ErrnoException).code) { case 'EACCES': serverLogger.error(`You do not have permission to listen on port ${config.port}.`); break; @@ -164,5 +164,5 @@ export default () => new Promise(resolve => { } }); - server.listen(config.port, resolve); + server.listen(config.port, () => resolve()); }); diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts index 1ea3aa2b2..2411d438d 100644 --- a/packages/backend/src/server/nodeinfo.ts +++ b/packages/backend/src/server/nodeinfo.ts @@ -3,7 +3,7 @@ import { IsNull, MoreThan } from 'typeorm'; import config from '@/config/index.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Users, Notes } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; +import { MONTH, DAY } from '@/const.js'; const router = new Router(); @@ -18,7 +18,33 @@ export const links = [{ href: config.url + nodeinfo2_0path, }]; -const nodeinfo2 = async () => { +const repository = 'https://akkoma.dev/FoundKeyGang/FoundKey'; + +type NodeInfo2Base = { + software: { + name: string; + version: string; + repository?: string; + }; + protocols: string[]; + services: { + inbound: string[]; + outbound: string[]; + }; + openRegistrations: boolean; + usage: { + users: { + total: number; + activeHalfyear: number; + activeMonth: number; + }; + localPosts: number; + localComments: number; + }; + metadata: Record; +}; + +const nodeinfo2 = async (): Promise => { const now = Date.now(); const [ meta, @@ -29,8 +55,8 @@ const nodeinfo2 = async () => { ] = await Promise.all([ fetchMeta(true), Users.count({ where: { host: IsNull() } }), - Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 15552000000)) } }), - Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 2592000000)) } }), + Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 180 * DAY)) } }), + Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - MONTH)) } }), Notes.count({ where: { userHost: IsNull() } }), ]); @@ -40,7 +66,7 @@ const nodeinfo2 = async () => { software: { name: 'foundkey', version: config.version, - repository: 'https://akkoma.dev/FoundKeyGang/FoundKey', + repository, }, protocols: ['activitypub'], services: { @@ -62,7 +88,7 @@ const nodeinfo2 = async () => { }, langs: meta.langs, tosUrl: meta.ToSUrl, - repositoryUrl: meta.repositoryUrl, + repositoryUrl: repository, feedbackUrl: 'ircs://irc.akkoma.dev/foundkey', disableRegistration: meta.disableRegistration, disableLocalTimeline: meta.disableLocalTimeline, @@ -76,23 +102,21 @@ const nodeinfo2 = async () => { enableDiscordIntegration: meta.enableDiscordIntegration, enableEmail: meta.enableEmail, enableServiceWorker: meta.enableServiceWorker, - proxyAccountName: proxyAccount ? proxyAccount.username : null, + proxyAccountName: proxyAccount?.username ?? null, themeColor: meta.themeColor || '#86b300', }, }; }; -const cache = new Cache>>(1000 * 60 * 10); - router.get(nodeinfo2_1path, async ctx => { - const base = await cache.fetch(null, () => nodeinfo2()); + const base = await nodeinfo2(); ctx.body = { version: '2.1', ...base }; ctx.set('Cache-Control', 'public, max-age=600'); }); router.get(nodeinfo2_0path, async ctx => { - const base = await cache.fetch(null, () => nodeinfo2()); + const base = await nodeinfo2(); delete base.software.repository; diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts index 8665f5d6f..1550e3022 100644 --- a/packages/backend/src/services/blocking/create.ts +++ b/packages/backend/src/services/blocking/create.ts @@ -12,7 +12,7 @@ import { perUserFollowingChart } from '@/services/chart/index.js'; import { genId } from '@/misc/gen-id.js'; import { getActiveWebhooks } from '@/misc/webhook-cache.js'; -export default async function(blocker: User, blockee: User) { +export default async function(blocker: User, blockee: User): Promise { await Promise.all([ cancelRequest(blocker, blockee), cancelRequest(blockee, blocker), @@ -32,13 +32,13 @@ export default async function(blocker: User, blockee: User) { await Blockings.insert(blocking); - if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { + if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee) && blocker.federateBlocks) { const content = renderActivity(renderBlock(blocking)); deliver(blocker, content, blockee.inbox); } } -async function cancelRequest(follower: User, followee: User) { +async function cancelRequest(follower: User, followee: User): Promise { const request = await FollowRequests.findOneBy({ followeeId: followee.id, followerId: follower.id, @@ -75,20 +75,20 @@ async function cancelRequest(follower: User, followee: User) { }); } - // リモートにフォローリクエストをしていたらUndoFollow送信 + // Send Undo Follow if followee is remote if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } - // リモートからフォローリクエストを受けていたらReject送信 + // Send Reject if follower is remote if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId!), followee)); deliver(followee, content, follower.inbox); } } -async function unFollow(follower: User, followee: User) { +async function unFollow(follower: User, followee: User): Promise { const following = await Followings.findOneBy({ followerId: follower.id, followeeId: followee.id, @@ -122,14 +122,14 @@ async function unFollow(follower: User, followee: User) { }); } - // リモートにフォローをしていたらUndoFollow送信 + // Send Undo Follow if follower is remote if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } } -async function removeFromList(listOwner: User, user: User) { +async function removeFromList(listOwner: User, user: User): Promise { const userLists = await UserLists.findBy({ userId: listOwner.id, }); diff --git a/packages/backend/src/services/chart/core.ts b/packages/backend/src/services/chart/core.ts index cd8d2a5a2..82b964373 100644 --- a/packages/backend/src/services/chart/core.ts +++ b/packages/backend/src/services/chart/core.ts @@ -1,5 +1,5 @@ /** - * チャートエンジン + * chart engine * * Tests located in test/chart */ @@ -24,7 +24,7 @@ type Schema = Record; @@ -42,12 +42,12 @@ type RawRecord = { id: number; /** - * 集計のグループ + * aggregation group */ group?: string | null; /** - * 集計日時のUnixタイムスタンプ(秒) + * Unix epoch timestamp (seconds) of aggregation */ date: number; } & TempColumnsForUnique & Columns; @@ -108,7 +108,7 @@ export function getJsonSchema(schema: S): ToJsonSchema { @@ -119,19 +119,23 @@ export default abstract class Chart { diff: Commit; group: string | null; }[] = []; - // ↓にしたいけどfindOneとかで型エラーになる - //private repositoryForHour: Repository>; - //private repositoryForDay: Repository>; + + /* + * The following would be nice but it gives a type error when used with findOne + *private repositoryForHour: Repository>; + *private repositoryForDay: Repository>; + */ + private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>; private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>; /** - * 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用) + * Computation to run once a day. Intended to fix discrepancies e.g. due to cascaded deletes or other changes that were missed. */ protected abstract tickMajor(group: string | null): Promise>>; /** - * 少なくとも最小スパン内に1回は実行されて欲しい計算処理を入れる + * A smaller computation that should be run once per lowest time interval. */ protected abstract tickMinor(group: string | null): Promise>>; @@ -274,7 +278,7 @@ export default abstract class Chart { } /** - * 現在(=今のHour or Day)のログをデータベースから探して、あればそれを返し、なければ作成して返します。 + * Search the database for the current (=current Hour or Day) log and return it if available, otherwise create and return it. */ private async claimCurrentLog(group: string | null, span: 'hour' | 'day'): Promise> { const [y, m, d, h] = Chart.getCurrentDate(); @@ -289,13 +293,13 @@ export default abstract class Chart { span === 'day' ? this.repositoryForDay : new Error('not happen') as never; - // 現在(=今のHour or Day)のログ + // current hour or day log entry const currentLog = await repository.findOneBy({ date: Chart.dateToTimestamp(current), ...(group ? { group } : {}), }) as RawRecord | undefined; - // ログがあればそれを返して終了 + // If logs are available, return them and exit. if (currentLog != null) { return currentLog; } @@ -303,12 +307,13 @@ export default abstract class Chart { let log: RawRecord; let data: KVs; - // 集計期間が変わってから、初めてのチャート更新なら - // 最も最近のログを持ってくる - // * 例えば集計期間が「日」である場合で考えると、 - // * 昨日何もチャートを更新するような出来事がなかった場合は、 - // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、 - // * 「昨日の」と決め打ちせずに「もっとも最近の」とします + // If this is the first chart update since the start of the aggregation period, + // use the most recent log entry. + // + // For example, if the aggregation period is "day", if nothing happened yesterday + // to change the chart, the log entry is not created in the first place. So "most + // recent" is used instead of "yesterdays" because there might be missing log + // entries. const latest = await this.getLatestLog(group, span); if (latest != null) { @@ -329,13 +334,13 @@ export default abstract class Chart { const unlock = await getChartInsertLock(lockKey); try { - // ロック内でもう1回チェックする + // check once more now that we're holding the lock const currentLog = await repository.findOneBy({ date, ...(group ? { group } : {}), }) as RawRecord | undefined; - // ログがあればそれを返して終了 + // if log entries are available now, return them and exit if (currentLog != null) return currentLog; const columns = {} as Record; @@ -344,7 +349,7 @@ export default abstract class Chart { columns[columnPrefix + name] = v; } - // 新規ログ挿入 + // insert new entries log = await repository.insert({ date, ...(group ? { group } : {}), @@ -374,11 +379,14 @@ export default abstract class Chart { return; } - // TODO: 前の時間のログがbufferにあった場合のハンドリング - // 例えば、save が20分ごとに行われるとして、前回行われたのは 01:50 だったとする。 - // 次に save が行われるのは 02:10 ということになるが、もし 01:55 に新規ログが buffer に追加されたとすると、 - // そのログは本来は 01:00~ のログとしてDBに保存されて欲しいのに、02:00~ のログ扱いになってしまう。 - // これを回避するための実装は複雑になりそうなため、一旦保留。 + // TODO: handling of previous time logs in buffer + + // For example, suppose that a save is performed every 20 minutes, and the last + // save was performed at 01:50. If a new log is added to the buffer at 01:55, the + // next save will take place at 02:10, and if the new log is added to the buffer + // at 01:55, then If a new log is added to the buffer at 01:55, the log is + // treated as a 02:00~ log, even though it should be saved as a 01:00~ log. The + // implementation to work around this is pending, as it would be complicated. const update = async (logHour: RawRecord, logDay: RawRecord): Promise => { const finalDiffs = {} as Record; @@ -406,9 +414,9 @@ export default abstract class Chart { if (v < 0) queryForHour[name] = () => `"${name}" - ${Math.abs(v)}`; if (v > 0) queryForDay[name] = () => `"${name}" + ${v}`; if (v < 0) queryForDay[name] = () => `"${name}" - ${Math.abs(v)}`; - } else if (Array.isArray(v) && v.length > 0) { // ユニークインクリメント + } else if (Array.isArray(v) && v.length > 0) { // unique increment const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique; - // TODO: item をSQLエスケープ + // TODO: SQL escape for item const itemsForHour = v.filter(item => !logHour[tempColumnName].includes(item)).map(item => `"${item}"`); const itemsForDay = v.filter(item => !logDay[tempColumnName].includes(item)).map(item => `"${item}"`); if (itemsForHour.length > 0) queryForHour[tempColumnName] = () => `array_cat("${tempColumnName}", '{${itemsForHour.join(',')}}'::varchar[])`; @@ -427,7 +435,7 @@ export default abstract class Chart { } // compute intersection - // TODO: intersectionに指定されたカラムがintersectionだった場合の対応 + // TODO: what to do if the column specified for intersection is an intersection itself for (const [k, v] of Object.entries(this.schema)) { const intersection = v.intersection; if (intersection) { @@ -455,7 +463,7 @@ export default abstract class Chart { } } - // ログ更新 + // update log await Promise.all([ this.repositoryForHour.createQueryBuilder() .update() @@ -471,7 +479,7 @@ export default abstract class Chart { logger.info(`${this.name + (logHour.group ? `:${logHour.group}` : '')}: Updated`); - // TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする + // TODO: do not delete anything new in the buffer since this round of processing began this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group)); }; @@ -528,7 +536,7 @@ export default abstract class Chart { public async clean(): Promise { const current = dateUTC(Chart.getCurrentDate()); - // 一日以上前かつ三日以内 + // more than 1 day and less than 3 days const gt = Chart.dateToTimestamp(current) - (60 * 60 * 24 * 3); const lt = Chart.dateToTimestamp(current) - (60 * 60 * 24); @@ -576,7 +584,7 @@ export default abstract class Chart { span === 'day' ? this.repositoryForDay : new Error('not happen') as never; - // ログ取得 + // gathering logs let logs = await repository.find({ where: { date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt)), @@ -587,10 +595,10 @@ export default abstract class Chart { }, }) as RawRecord[]; - // 要求された範囲にログがひとつもなかったら + // If there is no log entry in the requested range if (logs.length === 0) { - // もっとも新しいログを持ってくる - // (すくなくともひとつログが無いと隙間埋めできないため) + // Use the most recent logs instead. + // (At least 1 log entry is needed to fill the gap.) const recentLog = await repository.findOne({ where: group ? { group } : {}, order: { @@ -602,10 +610,10 @@ export default abstract class Chart { logs = [recentLog]; } - // 要求された範囲の最も古い箇所に位置するログが存在しなかったら + // If there is no log located at the earliest point in the requested range } else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) { - // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する - // (隙間埋めできないため) + // Bring the most recent log as of the earliest point in the requested range and append it to the end. + // (Due to inability to fill gaps) const outdatedLog = await repository.findOne({ where: { date: LessThan(Chart.dateToTimestamp(gt)), @@ -634,7 +642,7 @@ export default abstract class Chart { if (log) { chart.unshift(this.convertRawRecord(log)); } else { - // 隙間埋め + // fill in gaps const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current)); const data = latest ? this.convertRawRecord(latest) : null; chart.unshift(this.getNewLog(data)); @@ -644,10 +652,10 @@ export default abstract class Chart { const res = {} as ChartResult; /** + * Turn * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }] - * を + * into * { foo: [1, 2, 3], bar: [5, 6, 7] } - * にする */ for (const record of chart) { for (const [k, v] of Object.entries(record) as ([keyof typeof record, number])[]) { diff --git a/packages/backend/src/services/instance-actor.ts b/packages/backend/src/services/instance-actor.ts index 6d46fc24c..874952be0 100644 --- a/packages/backend/src/services/instance-actor.ts +++ b/packages/backend/src/services/instance-actor.ts @@ -1,28 +1,20 @@ import { IsNull } from 'typeorm'; import { ILocalUser } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; import { createSystemUser } from './create-system-user.js'; const ACTOR_USERNAME = 'instance.actor' as const; -const cache = new Cache(Infinity); +let instanceActor = await Users.findOneBy({ + host: IsNull(), + username: ACTOR_USERNAME, +}) as ILocalUser | undefined; export async function getInstanceActor(): Promise { - const cached = cache.get(null); - if (cached) return cached; - - const user = await Users.findOneBy({ - host: IsNull(), - username: ACTOR_USERNAME, - }) as ILocalUser | undefined; - - if (user) { - cache.set(null, user); - return user; + if (instanceActor) { + return instanceActor; } else { - const created = await createSystemUser(ACTOR_USERNAME) as ILocalUser; - cache.set(null, created); + instanceActor = await createSystemUser(ACTOR_USERNAME) as ILocalUser; return created; } } diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index 9bdae345a..bdae3f876 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -28,9 +28,9 @@ export default class Logger { if (config.syslog) { this.syslogClient = new SyslogPro.RFC5424({ - applacationName: 'FoundKey', + applicationName: 'FoundKey', timestamp: true, - encludeStructuredData: true, + includeStructuredData: true, color: true, extendedColor: true, server: { diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 1b987b5cc..e7dac0886 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -35,13 +35,23 @@ import { webhookDeliver } from '@/queue/index.js'; 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 { MINUTE } from '@/const.js'; import { updateHashtags } from '../update-hashtag.js'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; import { createNotification } from '../create-notification.js'; import { addNoteToAntenna } from '../add-note-to-antenna.js'; import { deliverToRelays } from '../relay.js'; -const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); +const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>( + 5 * MINUTE, + () => UserProfiles.find({ + where: { + enableWordMute: true, + }, + select: ['userId', 'mutedWords'], + }), +); type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -59,14 +69,14 @@ class NotificationManager { this.queue = []; } - public push(notifiee: ILocalUser['id'], reason: NotificationType) { - // 自分自身へは通知しない + public push(notifiee: ILocalUser['id'], reason: NotificationType): void { + // No notification to yourself. if (this.notifier.id === notifiee) return; const exist = this.queue.find(x => x.target === notifiee); if (exist) { - // 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする + // If you have been "mentioned and replied to," make the notification as a reply, not as a mention. if (reason !== 'mention') { exist.reason = reason; } @@ -78,7 +88,7 @@ class NotificationManager { } } - public async deliver() { + public async deliver(): Promise { for (const x of this.queue) { // check if the sender or thread are muted const userMuted = await Mutings.findOneBy({ @@ -119,7 +129,7 @@ type Option = { poll?: IPoll | null; localOnly?: boolean | null; cw?: string | null; - visibility?: string; + visibility?: 'home' | 'public' | 'followers' | 'specified'; visibleUsers?: MinimumUser[] | null; channel?: Channel | null; apMentions?: MinimumUser[] | null; @@ -130,9 +140,9 @@ type Option = { app?: App | null; }; -export default async (user: { id: User['id']; username: User['username']; host: User['host']; isSilenced: User['isSilenced']; createdAt: User['createdAt']; }, data: Option, silent = false) => new Promise(async (res, rej) => { - // チャンネル外にリプライしたら対象のスコープに合わせる - // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) +export default async (user: { id: User['id']; username: User['username']; host: User['host']; isSilenced: User['isSilenced']; createdAt: User['createdAt']; }, data: Option, silent = false): Promise => new Promise(async (res, rej) => { + // If you reply outside the channel, adjust to the scope of the target + // (I think this could be done client-side, but server-side for now) if (data.reply && data.channel && data.reply.channelId !== data.channel.id) { if (data.reply.channelId) { data.channel = await Channels.findOneBy({ id: data.reply.channelId }); @@ -141,9 +151,9 @@ export default async (user: { id: User['id']; username: User['username']; host: } } - // チャンネル内にリプライしたら対象のスコープに合わせる - // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) - if (data.reply && (data.channel == null) && data.reply.channelId) { + // When you reply to a channel, adjust the scope to that of the target. + // (I think this could be done client-side, but server-side for now) + if (data.reply?.channelId && (data.channel == null)) { data.channel = await Channels.findOneBy({ id: data.reply.channelId }); } @@ -154,32 +164,32 @@ export default async (user: { id: User['id']; username: User['username']; host: if (data.channel != null) data.visibleUsers = []; if (data.channel != null) data.localOnly = true; - // サイレンス + // silence if (user.isSilenced && data.visibility === 'public' && data.channel == null) { data.visibility = 'home'; } - // Renote対象が「ホームまたは全体」以外の公開範囲ならreject + // Reject if the target of the renote is not Home or Public. if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) { return rej('Renote target is not public or home'); } - // Renote対象がpublicではないならhomeにする + // If the target of the renote is not public, make it home. if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') { data.visibility = 'home'; } - // Renote対象がfollowersならfollowersにする + // If the target of Renote is followers, make it followers. if (data.renote && data.renote.visibility === 'followers') { data.visibility = 'followers'; } - // ローカルのみをRenoteしたらローカルのみにする + // Ff the original note is local-only, make the renote also local-only. if (data.renote && data.renote.localOnly && data.channel == null) { data.localOnly = true; } - // ローカルのみにリプライしたらローカルのみにする + // If you reply to local only, make it local only. if (data.reply && data.reply.localOnly && data.channel == null) { data.localOnly = true; } @@ -196,10 +206,10 @@ export default async (user: { id: User['id']; username: User['username']; host: // Parse MFM if needed if (!tags || !emojis || !mentionedUsers) { - const tokens = data.text ? mfm.parse(data.text)! : []; - const cwTokens = data.cw ? mfm.parse(data.cw)! : []; - const choiceTokens = data.poll && data.poll.choices - ? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) + const tokens = data.text ? mfm.parse(data.text) : []; + const cwTokens = data.cw ? mfm.parse(data.cw) : []; + const choiceTokens = data.poll?.choices + ? concat(data.poll.choices.map(choice => mfm.parse(choice))) : []; const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); @@ -213,8 +223,8 @@ export default async (user: { id: User['id']; username: User['username']; host: tags = tags.filter(tag => Array.from(tag || '').length <= 128).splice(0, 32); - if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { - mentionedUsers.push(await Users.findOneByOrFail({ id: data.reply!.userId })); + if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply?.userId)) { + mentionedUsers.push(await Users.findOneByOrFail({ id: data.reply.userId })); } if (data.visibility === 'specified') { @@ -226,8 +236,8 @@ export default async (user: { id: User['id']; username: User['username']; host: } } - if (data.reply && !data.visibleUsers.some(x => x.id === data.reply!.userId)) { - data.visibleUsers.push(await Users.findOneByOrFail({ id: data.reply!.userId })); + if (data.reply && !data.visibleUsers.some(x => x.id === data.reply?.userId)) { + data.visibleUsers.push(await Users.findOneByOrFail({ id: data.reply.userId })); } } @@ -235,7 +245,7 @@ export default async (user: { id: User['id']; username: User['username']; host: res(note); - // 統計を更新 + // Update Statistics notesChart.update(note, true); perUserNotesChart.update(user, note, true); @@ -247,7 +257,7 @@ export default async (user: { id: User['id']; username: User['username']; host: }); } - // ハッシュタグ更新 + // Hashtag Update if (data.visibility === 'public' || data.visibility === 'home') { updateHashtags(user, tags); } @@ -256,12 +266,7 @@ export default async (user: { id: User['id']; username: User['username']; host: incNotesCountOfUser(user); // Word mute - mutedWordsCache.fetch(null, () => UserProfiles.find({ - where: { - enableWordMute: true, - }, - select: ['userId', 'mutedWords'], - })).then(us => { + mutedWordsCache.fetch(null).then(us => { for (const u of us) { checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => { if (shouldMute) { @@ -301,7 +306,7 @@ export default async (user: { id: User['id']; username: User['username']; host: saveReply(data.reply, note); } - // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき + // When there is no re-note of the specified note by the specified user except for this post if (data.renote && (await countSameRenotes(user.id, data.renote.id, note.id) === 0)) { incRenoteCount(data.renote); } @@ -319,12 +324,12 @@ export default async (user: { id: User['id']; username: User['username']; host: if (!silent) { if (Users.isLocalUser(user)) activeUsersChart.write(user); - // 未読通知を作成 + // Create unread notifications if (data.visibility === 'specified') { if (data.visibleUsers == null) throw new Error('invalid param'); for (const u of data.visibleUsers) { - // ローカルユーザーのみ + // Local users only if (!Users.isLocalUser(u)) continue; insertNoteUnread(u.id, note, { @@ -334,7 +339,7 @@ export default async (user: { id: User['id']; username: User['username']; host: } } else { for (const u of mentionedUsers) { - // ローカルユーザーのみ + // Local users only if (!Users.isLocalUser(u)) continue; insertNoteUnread(u.id, note, { @@ -423,24 +428,24 @@ export default async (user: { id: User['id']; username: User['username']; host: const noteActivity = await renderNoteOrRenoteActivity(data, note); const dm = new DeliverManager(user, noteActivity); - // メンションされたリモートユーザーに配送 + // Delivered to remote users who have been mentioned for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) { dm.addDirectRecipe(u as IRemoteUser); } - // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 + // If the post is a reply and the poster is a local user and the poster of the post to which you are replying is a remote user, deliver if (data.reply && data.reply.userHost !== null) { const u = await Users.findOneBy({ id: data.reply.userId }); if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); } - // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 + // If the post is a Renote and the poster is a local user and the poster of the original Renote post is a remote user, deliver if (data.renote && data.renote.userHost !== null) { const u = await Users.findOneBy({ id: data.renote.userId }); if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); } - // フォロワーに配送 + // Deliver to followers if (['public', 'home', 'followers'].includes(note.visibility)) { dm.addFollowersRecipe(); } @@ -461,23 +466,23 @@ export default async (user: { id: User['id']; username: User['username']; host: lastNotedAt: new Date(), }); - Notes.countBy({ + const count = await Notes.countBy({ userId: user.id, channelId: data.channel.id, - }).then(count => { - // この処理が行われるのはノート作成後なので、ノートが一つしかなかったら最初の投稿だと判断できる - // TODO: とはいえノートを削除して何回も投稿すればその分だけインクリメントされる雑さもあるのでどうにかしたい - if (count === 1) { - Channels.increment({ id: data.channel!.id }, 'usersCount', 1); - } }); + + // This process takes place after the note is created, so if there is only one note, you can determine that it is the first submission. + // TODO: but there's also the messiness of deleting a note and posting it multiple times, which is incremented by the number of times it's posted, so I'd like to do something about that. + if (count === 1) { + Channels.increment({ id: data.channel.id }, 'usersCount', 1); + } } // Register to search database index(note); }); -async function renderNoteOrRenoteActivity(data: Option, note: 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) @@ -487,7 +492,7 @@ async function renderNoteOrRenoteActivity(data: Option, note: Note) { return renderActivity(content); } -function incRenoteCount(renote: Note) { +function incRenoteCount(renote: Note): void { Notes.createQueryBuilder().update() .set({ renoteCount: () => '"renoteCount" + 1', @@ -497,19 +502,17 @@ function incRenoteCount(renote: Note) { .execute(); } -async function insertNote(user: { id: User['id']; host: User['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) { +async function insertNote(user: { id: User['id']; host: User['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]): Promise { + const createdAt = data.createdAt ?? new Date(); + const insert = new Note({ - id: genId(data.createdAt!), - createdAt: data.createdAt!, - fileIds: data.files ? data.files.map(file => file.id) : [], - replyId: data.reply ? data.reply.id : null, - renoteId: data.renote ? data.renote.id : null, - channelId: data.channel ? data.channel.id : null, - threadId: data.reply - ? data.reply.threadId - ? data.reply.threadId - : data.reply.id - : null, + id: genId(createdAt), + createdAt, + fileIds: data.files?.map(file => file.id) ?? [], + replyId: data.reply?.id ?? null, + renoteId: data.renote?.id ?? null, + channelId: data.channel?.id ?? null, + threadId: data.reply?.threadId ?? data.reply?.id ?? null, name: data.name, text: data.text, hasPoll: data.poll != null, @@ -517,15 +520,13 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O tags: tags.map(tag => normalizeForSearch(tag)), emojis, userId: user.id, - localOnly: data.localOnly!, - visibility: data.visibility as any, + localOnly: data.localOnly ?? false, + visibility: data.visibility, visibleUserIds: data.visibility === 'specified' - ? data.visibleUsers - ? data.visibleUsers.map(u => u.id) - : [] + ? data.visibleUsers?.map(u => u.id) ?? [] : [], - attachedFileTypes: data.files ? data.files.map(file => file.type) : [], + attachedFileTypes: data.files?.map(file => file.type) ?? [], // denormalized data below replyUserId: data.reply?.userId, @@ -543,29 +544,26 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O insert.mentions = mentionedUsers.map(u => u.id); } - // 投稿を作成 + // Create a post try { - if (insert.hasPoll) { - // Start transaction - await db.transaction(async transactionalEntityManager => { - await transactionalEntityManager.insert(Note, insert); + // Start transaction + await db.transaction(async transactionalEntityManager => { + await transactionalEntityManager.insert(Note, insert); + if (data.poll != null) { const poll = new Poll({ noteId: insert.id, - choices: data.poll!.choices, - expiresAt: data.poll!.expiresAt, - multiple: data.poll!.multiple, - votes: new Array(data.poll!.choices.length).fill(0), + choices: data.poll.choices, + expiresAt: data.poll.expiresAt, + multiple: data.poll.multiple, + votes: new Array(data.poll.choices.length).fill(0), noteVisibility: insert.visibility, userId: user.id, userHost: user.host, }); - await transactionalEntityManager.insert(Poll, poll); - }); - } else { - await Notes.insert(insert); - } + } + }); return insert; } catch (e) { @@ -582,10 +580,10 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O } } -function index(note: Note) { +function index(note: Note): void { if (note.text == null || config.elasticsearch == null) return; - es!.index({ + es.index({ index: config.elasticsearch.index || 'misskey_note', id: note.id.toString(), body: { @@ -596,7 +594,7 @@ function index(note: Note) { }); } -async function notifyToWatchersOfRenotee(renote: Note, user: { id: User['id']; }, nm: NotificationManager, type: NotificationType) { +async function notifyToWatchersOfRenotee(renote: Note, user: { id: User['id']; }, nm: NotificationManager, type: NotificationType): Promise { const watchers = await NoteWatchings.findBy({ noteId: renote.id, userId: Not(user.id), @@ -607,7 +605,7 @@ async function notifyToWatchersOfRenotee(renote: Note, user: { id: User['id']; } } } -async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; }, nm: NotificationManager) { +async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; }, nm: NotificationManager): Promise { const watchers = await NoteWatchings.findBy({ noteId: reply.id, userId: Not(user.id), @@ -618,7 +616,7 @@ async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; }, } } -async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) { +async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager): Promise { for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) { const threadMuted = await NoteThreadMutings.findOneBy({ userId: u.id, @@ -653,11 +651,11 @@ async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, } } -function saveReply(reply: Note, note: Note) { +function saveReply(reply: Note, note: Note): void { Notes.increment({ id: reply.id }, 'repliesCount', 1); } -function incNotesCountOfUser(user: { id: User['id']; }) { +function incNotesCountOfUser(user: { id: User['id']; }): void { Users.createQueryBuilder().update() .set({ updatedAt: new Date(), @@ -668,7 +666,7 @@ function incNotesCountOfUser(user: { id: User['id']; }) { } async function extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise { - if (tokens == null) return []; + if (tokens.length === 0) return []; const mentions = extractMentions(tokens); diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index 1fd1d2900..af9ad350a 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -1,4 +1,4 @@ -import { Brackets, In, IsNull, Not } from 'typeorm'; +import { Brackets, FindOptionsWhere, In, IsNull, Not } from 'typeorm'; import { publishNoteStream } from '@/services/stream.js'; import renderDelete from '@/remote/activitypub/renderer/delete.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; @@ -17,14 +17,14 @@ import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js import { deliverToRelays } from '../relay.js'; /** - * 投稿を削除します。 - * @param user 投稿者 - * @param note 投稿 + * Delete your note. + * @param user author + * @param note note to be deleted */ export default async function(user: { id: User['id']; uri: User['uri']; host: User['host']; }, note: Note, quiet = false): Promise { const deletedAt = new Date(); - // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき + // If this is the only renote of this note by this user if (note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id)) === 0) { Notes.decrement({ id: note.renoteId }, 'renoteCount', 1); Notes.decrement({ id: note.renoteId }, 'score', 1); @@ -37,15 +37,13 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us if (!quiet) { publishNoteStream(note.id, 'deleted', { deletedAt }); - //#region ローカルの投稿なら削除アクティビティを配送 + // deliver delete activity of note itself for local posts if (Users.isLocalUser(user) && !note.localOnly) { let renote: Note | null = null; - // if deletd note is renote + // if deleted note is renote if (isPureRenote(note)) { - renote = await Notes.findOneBy({ - id: note.renoteId, - }); + renote = await Notes.findOneBy({ id: note.renoteId }); } const content = renderActivity(renote @@ -55,17 +53,14 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us deliverToConcerned(user, note, content); } - // also deliever delete activity to cascaded notes - const cascadingNotes = (await findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes + // also deliver delete activity to cascaded notes + const cascadingNotes = await findCascadingNotes(note); for (const cascadingNote of cascadingNotes) { - if (!cascadingNote.user) continue; - if (!Users.isLocalUser(cascadingNote.user)) continue; const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); deliverToConcerned(cascadingNote.user, cascadingNote, content); } - //#endregion - // 統計を更新 + // update statistics notesChart.update(note, false); perUserNotesChart.update(user, note, false); @@ -83,30 +78,47 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us }); } +/** + * Search for notes that will be affected by ON CASCADE DELETE. + * However, only notes for which it is relevant to deliver delete activities are searched. + * This means only local notes that are not local-only are searched. + */ async function findCascadingNotes(note: Note): Promise { const cascadingNotes: Note[] = []; const recursive = async (noteId: string): Promise => { - const query = Notes.createQueryBuilder('note') - .where('note.replyId = :noteId', { noteId }) - .orWhere(new Brackets(q => { - q.where('note.renoteId = :noteId', { noteId }) - .andWhere('note.text IS NOT NULL'); - })) - .leftJoinAndSelect('note.user', 'user'); - const replies = await query.getMany(); - for (const reply of replies) { + // FIXME: use note_replies SQL function? Unclear what to do with 2nd and 3rd parameter, maybe rewrite the function. + const replies = await Notes.find({ + where: [{ + replyId: noteId, + localOnly: false, + userHost: IsNull(), + }, { + renoteId: noteId, + text: Not(IsNull()), + localOnly: false, + userHost: IsNull(), + }], + relations: { + user: true, + }, + }); + + await Promise.all(replies.map(reply => { + // only add unique notes + if (cascadingNotes.find((x) => x.id == reply.id) != null) return; + cascadingNotes.push(reply); - await recursive(reply.id); - } + return recursive(reply.id); + })); }; await recursive(note.id); - return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users + return cascadingNotes; } async function getMentionedRemoteUsers(note: Note): Promise { - const where = [] as any[]; + const where: FindOptionsWhere[] = []; // mention / reply / dm if (note.mentions.length > 0) { diff --git a/packages/backend/src/services/note/polls/vote.ts b/packages/backend/src/services/note/polls/vote.ts index 3382e10bd..5e27159e8 100644 --- a/packages/backend/src/services/note/polls/vote.ts +++ b/packages/backend/src/services/note/polls/vote.ts @@ -6,7 +6,7 @@ import { PollVotes, NoteWatchings, Polls, Blockings, NoteThreadMutings } from '@ import { genId } from '@/misc/gen-id.js'; import { createNotification } from '@/services/create-notification.js'; -export default async function(user: CacheableUser, note: Note, choice: number) { +export async function vote(user: CacheableUser, note: Note, choice: number): Promise { const poll = await Polls.findOneBy({ noteId: note.id }); if (poll == null) throw new Error('poll not found'); diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts index 5cc4f331c..df930c15c 100644 --- a/packages/backend/src/services/note/reaction/create.ts +++ b/packages/backend/src/services/note/reaction/create.ts @@ -13,9 +13,9 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js import { NoteReaction } from '@/models/entities/note-reaction.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { createNotification } from '@/services/create-notification.js'; -import deleteReaction from './delete.js'; +import { deleteReaction } from './delete.js'; -export default async (user: { id: User['id']; host: User['host']; }, note: Note, reaction?: string) => { +export async function createReaction(user: { id: User['id']; host: User['host']; }, note: Note, reaction?: string): Promise { // Check blocking if (note.userId !== user.id) { const block = await Blockings.findOneBy({ @@ -148,4 +148,4 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note, dm.execute(); } //#endregion -}; +} diff --git a/packages/backend/src/services/note/reaction/delete.ts b/packages/backend/src/services/note/reaction/delete.ts index a7cbcb1c1..3fc85a3d1 100644 --- a/packages/backend/src/services/note/reaction/delete.ts +++ b/packages/backend/src/services/note/reaction/delete.ts @@ -9,7 +9,7 @@ import { Note } from '@/models/entities/note.js'; import { NoteReactions, Users, Notes } from '@/models/index.js'; import { decodeReaction } from '@/misc/reaction-lib.js'; -export default async (user: { id: User['id']; host: User['host']; }, note: Note) => { +export async function deleteReaction(user: { id: User['id']; host: User['host']; }, note: Note): Promise { // if already unreacted const exist = await NoteReactions.findOneBy({ noteId: note.id, @@ -55,4 +55,4 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note) dm.execute(); } //#endregion -}; +} diff --git a/packages/backend/src/services/note/read.ts b/packages/backend/src/services/note/read.ts index 0b678893c..6899eccc5 100644 --- a/packages/backend/src/services/note/read.ts +++ b/packages/backend/src/services/note/read.ts @@ -12,14 +12,14 @@ import { Packed } from '@/misc/schema.js'; /** * Mark notes as read */ -export default async function( +export async function readNote( userId: User['id'], notes: (Note | Packed<'Note'>)[], info?: { following: Set; followingChannels: Set; }, -) { +): Promise { const following = info?.following ? info.following : new Set((await Followings.find({ where: { followerId: userId, diff --git a/packages/backend/src/services/note/unwatch.ts b/packages/backend/src/services/note/unwatch.ts index 3964b2ba5..2f779586d 100644 --- a/packages/backend/src/services/note/unwatch.ts +++ b/packages/backend/src/services/note/unwatch.ts @@ -2,9 +2,9 @@ import { User } from '@/models/entities/user.js'; import { NoteWatchings } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; -export default async (me: User['id'], note: Note) => { +export async function unwatch(me: User['id'], note: Note): Promise { await NoteWatchings.delete({ noteId: note.id, userId: me, }); -}; +} diff --git a/packages/backend/src/services/note/watch.ts b/packages/backend/src/services/note/watch.ts index 2210c44a7..634870c75 100644 --- a/packages/backend/src/services/note/watch.ts +++ b/packages/backend/src/services/note/watch.ts @@ -4,8 +4,8 @@ import { NoteWatchings } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { NoteWatching } from '@/models/entities/note-watching.js'; -export default async (me: User['id'], note: Note) => { - // 自分の投稿はwatchできない +export async function watch(me: User['id'], note: Note): Promise { + // User can't watch their own posts. if (me === note.userId) { return; } @@ -17,4 +17,4 @@ export default async (me: User['id'], note: Note) => { userId: me, noteUserId: note.userId, } as NoteWatching); -}; +} diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts index 56a8c83bf..5f1e30982 100644 --- a/packages/backend/src/services/push-notification.ts +++ b/packages/backend/src/services/push-notification.ts @@ -15,20 +15,21 @@ type pushNotificationsTypes = { 'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string }; }; -// プッシュメッセージサーバーには文字数制限があるため、内容を削減します +// Reduce the content of the push message because of the character limit function truncateNotification(notification: Packed<'Notification'>): any { if (notification.note) { return { ...notification, note: { ...notification.note, - // textをgetNoteSummaryしたものに置き換える + // replace text with getNoteSummary text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note), cw: undefined, reply: undefined, renote: undefined, - user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる + // unnecessary, since usually the user who is receiving the notification knows who they are + user: undefined as any, }, }; } @@ -41,7 +42,7 @@ export async function pushNotification(u if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; - // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 + // Register key pair information push.setVapidDetails(config.url, meta.swPublicKey, meta.swPrivateKey); @@ -65,10 +66,6 @@ export async function pushNotification(u }), { proxy: config.proxy, }).catch((err: any) => { - //swLogger.info(err.statusCode); - //swLogger.info(err.headers); - //swLogger.info(err.body); - if (err.statusCode === 410) { SwSubscriptions.delete({ userId, diff --git a/packages/backend/src/services/register-or-fetch-instance-doc.ts b/packages/backend/src/services/register-or-fetch-instance-doc.ts index 75fea50f2..8c625ff21 100644 --- a/packages/backend/src/services/register-or-fetch-instance-doc.ts +++ b/packages/backend/src/services/register-or-fetch-instance-doc.ts @@ -3,29 +3,27 @@ import { Instances } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { toPuny } from '@/misc/convert-host.js'; import { Cache } from '@/misc/cache.js'; +import { HOUR } from '@/const.js'; -const cache = new Cache(1000 * 60 * 60); +const cache = new Cache( + HOUR, + (host) => Instances.findOneBy({ host }).then(x => x ?? undefined), +); export async function registerOrFetchInstanceDoc(idnHost: string): Promise { const host = toPuny(idnHost); - const cached = cache.get(host); + const cached = cache.fetch(host); if (cached) return cached; - const index = await Instances.findOneBy({ host }); + // apparently a new instance + const i = await Instances.insert({ + id: genId(), + host, + caughtAt: new Date(), + lastCommunicatedAt: new Date(), + }).then(x => Instances.findOneByOrFail(x.identifiers[0])); - if (index == null) { - const i = await Instances.insert({ - id: genId(), - host, - caughtAt: new Date(), - lastCommunicatedAt: new Date(), - }).then(x => Instances.findOneByOrFail(x.identifiers[0])); - - cache.set(host, i); - return i; - } else { - cache.set(host, index); - return index; - } + cache.set(host, i); + return i; } diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts index a05645f09..36bf88b9c 100644 --- a/packages/backend/src/services/relay.ts +++ b/packages/backend/src/services/relay.ts @@ -8,11 +8,21 @@ import { Users, Relays } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { Cache } from '@/misc/cache.js'; import { Relay } from '@/models/entities/relay.js'; +import { MINUTE } from '@/const.js'; import { createSystemUser } from './create-system-user.js'; const ACTOR_USERNAME = 'relay.actor' as const; -const relaysCache = new Cache(1000 * 60 * 10); +/** + * There is only one cache key: null. + * A cache is only used here to have expiring storage. + */ +const relaysCache = new Cache( + 10 * MINUTE, + () => Relays.findBy({ + status: 'accepted', + }), +); export async function getRelayActor(): Promise { const user = await Users.findOneBy({ @@ -83,9 +93,7 @@ export async function relayRejected(id: string) { export async function deliverToRelays(user: { id: User['id']; host: null; }, activity: any) { if (activity == null) return; - const relays = await relaysCache.fetch(null, () => Relays.findBy({ - status: 'accepted', - })); + const relays = await relaysCache.fetch(null); if (relays.length === 0) return; // TODO diff --git a/packages/backend/src/services/suspend-user.ts b/packages/backend/src/services/suspend-user.ts index b891a6a1c..80806cd3e 100644 --- a/packages/backend/src/services/suspend-user.ts +++ b/packages/backend/src/services/suspend-user.ts @@ -1,7 +1,7 @@ import { Not, IsNull } from 'typeorm'; import renderDelete from '@/remote/activitypub/renderer/delete.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { deliver } from '@/queue/index.js'; +import DeliverManager from '@/remote/activitypub/deliver-manager.js'; import config from '@/config/index.js'; import { User } from '@/models/entities/user.js'; import { Users, Followings } from '@/models/index.js'; @@ -11,27 +11,11 @@ export async function doPostSuspend(user: { id: User['id']; host: User['host'] } publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); if (Users.isLocalUser(user)) { - // 知り得る全SharedInboxにDelete配信 const content = renderActivity(renderDelete(`${config.url}/users/${user.id}`, user)); - const queue: string[] = []; - - const followings = await Followings.find({ - where: [ - { followerSharedInbox: Not(IsNull()) }, - { followeeSharedInbox: Not(IsNull()) }, - ], - select: ['followerSharedInbox', 'followeeSharedInbox'], - }); - - const inboxes = followings.map(x => x.followerSharedInbox || x.followeeSharedInbox); - - for (const inbox of inboxes) { - if (inbox != null && !queue.includes(inbox)) queue.push(inbox); - } - - for (const inbox of queue) { - deliver(user, content, inbox); - } + // deliver to all of known network + const dm = new DeliverManager(user, content); + dm.addEveryone(); + await dm.execute(); } } diff --git a/packages/backend/src/services/user-cache.ts b/packages/backend/src/services/user-cache.ts index d95a8968a..f7f58dba5 100644 --- a/packages/backend/src/services/user-cache.ts +++ b/packages/backend/src/services/user-cache.ts @@ -3,10 +3,18 @@ import { Users } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; import { subscriber } from '@/db/redis.js'; -export const userByIdCache = new Cache(Infinity); -export const localUserByNativeTokenCache = new Cache(Infinity); -export const localUserByIdCache = new Cache(Infinity); -export const uriPersonCache = new Cache(Infinity); +export const userByIdCache = new Cache( + Infinity, + (id) => Users.findOneBy({ id }).then(x => x ?? undefined), +); +export const localUserByNativeTokenCache = new Cache( + Infinity, + (token) => Users.findOneBy({ token }).then(x => x ?? undefined), +); +export const uriPersonCache = new Cache( + Infinity, + (uri) => Users.findOneBy({ uri }).then(x => x ?? undefined), +); subscriber.on('message', async (_, data) => { const obj = JSON.parse(data); @@ -27,7 +35,6 @@ subscriber.on('message', async (_, data) => { } if (Users.isLocalUser(user)) { localUserByNativeTokenCache.set(user.token, user); - localUserByIdCache.set(user.id, user); } break; } diff --git a/packages/backend/test/services/blocking.ts b/packages/backend/test/services/blocking.ts new file mode 100644 index 000000000..41e5ef5be --- /dev/null +++ b/packages/backend/test/services/blocking.ts @@ -0,0 +1,58 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import * as sinon from 'sinon'; +import { async, signup, startServer, shutdownServer, initTestDb } from '../utils.js'; + +describe('Creating a block activity', () => { + let p: childProcess.ChildProcess; + + // alice blocks bob + let alice: any; + let bob: any; + let carol: any; + + before(async () => { + await initTestDb(); + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + bob.host = 'http://remote'; + carol.host = 'http://remote'; + }); + + beforeEach(() => { + sinon.restore(); + }); + + after(async () => { + await shutdownServer(p); + }); + + it('Should federate blocks normally', async(async () => { + const createBlock = (await import('../../src/services/blocking/create')).default; + const deleteBlock = (await import('../../src/services/blocking/delete')).default; + + const queues = await import('../../src/queue/index'); + const spy = sinon.spy(queues, 'deliver'); + await createBlock(alice, bob); + assert(spy.calledOnce); + await deleteBlock(alice, bob); + assert(spy.calledTwice); + })); + + it('Should not federate blocks if federateBlocks is false', async () => { + const createBlock = (await import('../../src/services/blocking/create')).default; + const deleteBlock = (await import('../../src/services/blocking/delete')).default; + + alice.federateBlocks = true; + + const queues = await import('../../src/queue/index'); + const spy = sinon.spy(queues, 'deliver'); + await createBlock(alice, carol); + await deleteBlock(alice, carol); + assert(spy.notCalled); + }); +}); diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index c10dcfb06..88fc8387d 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -32,12 +32,8 @@ export async function signout() { const registration = await navigator.serviceWorker.ready; const push = await registration.pushManager.getSubscription(); if (push) { - await fetch(`${apiUrl}/sw/unregister`, { - method: 'POST', - body: JSON.stringify({ - i: $i.token, - endpoint: push.endpoint, - }), + await api('sw/unregister', { + endpoint: push.endpoint, }); } } @@ -79,13 +75,7 @@ export async function removeAccount(id: Account['id']) { function fetchAccount(token: string): Promise { return new Promise((done, fail) => { // Fetch user - fetch(`${apiUrl}/i`, { - method: 'POST', - body: JSON.stringify({ - i: token, - }), - }) - .then(res => res.json()) + api('i', {}, token) .then(res => { if (res.error) { if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') { diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index b246f4be4..4aa1e6c37 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -386,9 +386,7 @@ function onChangeFileInput() { } function upload(file: File, folderToUpload?: foundkey.entities.DriveFolder | null) { - uploadFile(file, (typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal).then(res => { - addFile(res, true); - }); + uploadFile(file, folderToUpload?.id ?? null, undefined, keepOriginal); } function chooseFile(file: foundkey.entities.DriveFile) { diff --git a/packages/client/src/components/follow-button.vue b/packages/client/src/components/follow-button.vue index 7c4f5abd3..938e73e27 100644 --- a/packages/client/src/components/follow-button.vue +++ b/packages/client/src/components/follow-button.vue @@ -5,26 +5,17 @@ :disabled="wait" @click="onClick" > - diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue index eed5323fa..d5c8c91c4 100644 --- a/packages/client/src/components/form/input.vue +++ b/packages/client/src/components/form/input.vue @@ -17,6 +17,7 @@ :spellcheck="spellcheck" :step="step" :list="id" + :maxlength="max" @focus="focused = true" @blur="focused = false" @keydown="onKeydown($event)" @@ -58,6 +59,7 @@ const props = defineProps<{ manualSave?: boolean; small?: boolean; large?: boolean; + max?: number; }>(); const emit = defineEmits<{ diff --git a/packages/client/src/components/form/textarea.vue b/packages/client/src/components/form/textarea.vue index 24fe867e3..b41ddf9e0 100644 --- a/packages/client/src/components/form/textarea.vue +++ b/packages/client/src/components/form/textarea.vue @@ -14,6 +14,7 @@ :pattern="pattern" :autocomplete="autocomplete ? 'on' : 'off'" :spellcheck="spellcheck" + :maxlength="max" @focus="focused = true" @blur="focused = false" @keydown="onKeydown($event)" @@ -54,6 +55,7 @@ const props = withDefaults(defineProps<{ pre?: boolean; debounce?: boolean; manualSave?: boolean; + max?: number; }>(), { pattern: undefined, placeholder: '', diff --git a/packages/client/src/components/reactions-viewer.reaction.vue b/packages/client/src/components/reactions-viewer.reaction.vue index a78efb145..685c90106 100644 --- a/packages/client/src/components/reactions-viewer.reaction.vue +++ b/packages/client/src/components/reactions-viewer.reaction.vue @@ -31,7 +31,7 @@ const buttonRef = ref(); const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); -const toggleReaction = () => { +const toggleReaction = (): void => { if (!canToggle.value) return; const oldReaction = props.note.myReaction; diff --git a/packages/client/src/components/signin.vue b/packages/client/src/components/signin.vue index 112e13bd3..bc4063f9b 100644 --- a/packages/client/src/components/signin.vue +++ b/packages/client/src/components/signin.vue @@ -8,7 +8,6 @@