diff --git a/COPYING b/COPYING index 40dbe8b30..658556e58 100644 --- a/COPYING +++ b/COPYING @@ -1,6 +1,6 @@ Unless otherwise stated this repository is Copyright © 2014-2022 syuilo and contributors -Copyright © 2022 FoundKey contributors +Copyright © 2022-2023 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. (You may be able to run `git shortlog -se` to see a full list of authors.) diff --git a/ROADMAP.md b/ROADMAP.md index 1c64c9417..b777512a1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,9 +3,9 @@ Note: this document is historical. Everything starting with the next section is the original "idea" document that led to the foundation of FoundKey. For the current status you should see the following: -* The Behavioral Fixes [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/3) -* The Technological Upkeep [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/4) -* The Features [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/5) +* Issues labeled with [behaviour-fix](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=44) +* Issues labeled with [upkeep](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=43) +* Issues labeled with [feature](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=42) ## Misskey Goals I’ve been thinking about a community misskey fork for a while now. To some of you, this is not a surprise. Let’s talk about that. diff --git a/docs/install.md b/docs/install.md index dbdf5e0d0..330c28633 100644 --- a/docs/install.md +++ b/docs/install.md @@ -70,6 +70,8 @@ Build foundkey with the following: `NODE_ENV=production yarn build` +If your system has at least 4GB of RAM, run `NODE_ENV=production yarn build-parallel` to speed up build times. + If you're still encountering errors about some modules, use node-gyp: 1. `npx node-gyp configure` @@ -182,6 +184,7 @@ Use git to pull in the latest changes and rerun the build and migration commands git pull git submodule update --init yarn install +# Use build-parallel if your system has 4GB or more RAM and want faster builds NODE_ENV=production yarn build yarn migrate ``` diff --git a/docs/moderation.md b/docs/moderation.md new file mode 100644 index 000000000..64cf096e9 --- /dev/null +++ b/docs/moderation.md @@ -0,0 +1,103 @@ +# User moderation + +A lot of the user moderation activities can be found on the `user-info` page. You can reach this page by going to a users profile page, open the three dot menu, select "About" and navigating to the "Moderation" section of the page that opens. +With the necessary privileges, this page will allow you to: +- Toggle whether a user is a moderator (administrators on local users only) +- Reset the users password (local users only) +- Delete a user (administrators only) +- Delete all files of a user + For remote users, cached files (if any) will be deleted. +- Silence a user + This disallows a user from making a note with `public` visibility. + If necessary the visibility of incoming notes or locally created notes will be lowered. +- Suspend a user + This will drop any incoming activities of this actor and hide them from public view on this instance. + +# Administrator + +When an instance is first set up, the initial user to be created will be made an administrator by default. +This means that typically the instance owner is the administrator. +It is also possible to have multiple administrators, however making a user an administrator is not implemented in the client. +To make a user an administrator, you will need access to the database. +This is intended for security reasons of +1. not exposing this very dangerous functionality via the API +2. making sure someone that has shell access to the server anyway "approves" this. + +To make a user an administrator, you will first need the user's ID. +To get it you can go to the user's profile page, open the three dot menu, select "About" and copy the ID displayed there. +Then, go to the database and run the following query, replacing `` with the ID gotten above. +```sql +UPDATE "user" SET "isAdmin" = true WHERE "id" = ''; +``` + +The user that was made administrator may need to reload their client to see the changes take effect. + +To demote a user, you can do a similar operation, but instead with `... SET "isAdmin" = false ...`. + +## Immunity + +- Cannot be reported by local users. +- Cannot have their password reset. + To see how you can reset an administrator password, see below. +- Cannot have their account deleted. +- Cannot be suspended. +- Cannot be silenced. +- Cannot have their account details viewed by moderators. +- Cannot be made moderators. + +## Abilities + +- Create or delete user accounts. +- Add or remove moderators. +- View and change instance configuration (e.g. Translation API keys). +- View all followers and followees. + +Administrators also have the same ability as moderators. +Note of course that people with access to the server and/or database access can do basically anything without restrictions (including breaking the instance). + +## Resetting an administrators password + +Administrators are blocked from the paths of resetting the password by moderators or administrators. +However, if your server has email configured you should be able to use the "Forgot password" link on the normal signin dialog. + +If you did not set up email, you will need to kick of this process instead through modifying the database yourself. +You will need the user ID whose password should be reset, indicated in the following as ``; +as well as a random string (a UUID would be recommended) indicated as ``. + +Replacing the two terms above, run the following SQL query: +```sql +INSERT INTO "password_reset_request" VALUES ('0000000000', now(), '', ''); +``` + +After that, navigate to `/reset-password/` on your instance to finish the password reset process. +After that you should be able to sign in with the new password you just set. + +# Moderator + +A moderator has fewer privileges than an administrator. +They can also be more easily added or removed by an adminstrator. +Having moderators may be a good idea to help with user moderation. + +## Immunity + +- Cannot be reported by local users. +- Cannot be suspended. + +## Abilities + +- Suspend users. +- Add, list and remove relays. +- View queue, database and server information. +- Create, edit, delete, export and import local custom emoji. +- View global, social and local timelines even if disabled by administrators. +- Show, update and delete any users files and file metadata. + Managing emoji is described in [a separate file](emoji.md). +- Delete any users notes. +- Create an invitation. + This allows users to register an account even if (public) registrations are closed using an invite code. +- View users' account details. +- Suspend and unsuspend users. +- Silence and unsilence users. +- Handle reports. +- Create, update and delete announcements. +- View the moderation log. diff --git a/locales/en-US.yml b/locales/en-US.yml index 885a487b4..ec0abd644 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -501,6 +501,7 @@ scratchpadDescription: "The Scratchpad provides an environment for AiScript expe \ in it." output: "Output" updateRemoteUser: "Update remote user information" +deleteAllFiles: "Delete all files" deleteAllFilesConfirm: "Are you sure that you want to delete all files?" removeAllFollowing: "Unfollow all followed users" removeAllFollowingDescription: "Executing this unfollows all accounts from {host}.\ diff --git a/package.json b/package.json index 4f47f3bf0..659ba2897 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "packages/*" ], "scripts": { - "build": "yarn workspaces foreach --parallel --topological run build && yarn run gulp", + "build": "yarn workspaces foreach --topological run build && yarn run gulp", + "build-parallel": "yarn workspaces foreach --parallel --topological run build && yarn run gulp", "start": "yarn workspace backend run start", "start:test": "yarn workspace backend run start:test", "init": "yarn migrate", diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs index 5a06889dc..c3b829966 100644 --- a/packages/backend/.eslintrc.cjs +++ b/packages/backend/.eslintrc.cjs @@ -6,7 +6,11 @@ module.exports = { extends: [ '../shared/.eslintrc.js', ], + plugins: [ + 'foundkey-custom-rules', + ], rules: { + 'foundkey-custom-rules/typeorm-prefer-count': 'error', 'import/order': ['warn', { 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], 'pathGroups': [ diff --git a/packages/backend/migration/1672607891750-remove-reversi.js b/packages/backend/migration/1672607891750-remove-reversi.js new file mode 100644 index 000000000..2906e884a --- /dev/null +++ b/packages/backend/migration/1672607891750-remove-reversi.js @@ -0,0 +1,21 @@ +export class removeReversi1672607891750 { + name = 'removeReversi1672607891750'; + + async up(queryRunner) { + await queryRunner.query(`DROP TABLE "reversi_matching"`); + await queryRunner.query(`DROP TABLE "reversi_game"`); + } + + async down(queryRunner) { + await queryRunner.query(`CREATE TABLE "reversi_game" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "startedAt" TIMESTAMP WITH TIME ZONE, "user1Id" character varying(32) NOT NULL, "user2Id" character varying(32) NOT NULL, "user1Accepted" boolean NOT NULL DEFAULT false, "user2Accepted" boolean NOT NULL DEFAULT false, "black" integer, "isStarted" boolean NOT NULL DEFAULT false, "isEnded" boolean NOT NULL DEFAULT false, "winnerId" character varying(32), "surrendered" character varying(32), "logs" jsonb NOT NULL DEFAULT '[]', "map" character varying(64) array NOT NULL, "bw" character varying(32) NOT NULL, "isLlotheo" boolean NOT NULL DEFAULT false, "canPutEverywhere" boolean NOT NULL DEFAULT false, "loopedBoard" boolean NOT NULL DEFAULT false, "form1" jsonb DEFAULT null, "form2" jsonb DEFAULT null, "crc32" character varying(32), CONSTRAINT "PK_76b30eeba71b1193ad7c5311c3f" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt")`); + await queryRunner.query(`CREATE TABLE "reversi_matching" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "parentId" character varying(32) NOT NULL, "childId" character varying(32) NOT NULL, CONSTRAINT "PK_880bd0afbab232f21c8b9d146cf" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt")`); + await queryRunner.query(`CREATE INDEX "IDX_3b25402709dd9882048c2bbade" ON "reversi_matching" ("parentId")`); + await queryRunner.query(`CREATE INDEX "IDX_e247b23a3c9b45f89ec1299d06" ON "reversi_matching" ("childId")`); + await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/migration/1672991292018-remove-user-group-invite.js b/packages/backend/migration/1672991292018-remove-user-group-invite.js new file mode 100644 index 000000000..adc5d935e --- /dev/null +++ b/packages/backend/migration/1672991292018-remove-user-group-invite.js @@ -0,0 +1,23 @@ +export class removeUserGroupInvite1672991292018 { + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_e10924607d058004304611a436a"`); + await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_1039988afa3bf991185b277fe03"`); + await queryRunner.query(`DROP INDEX "IDX_d9ecaed8c6dc43f3592c229282"`); + await queryRunner.query(`DROP INDEX "IDX_78787741f9010886796f2320a4"`); + await queryRunner.query(`DROP INDEX "IDX_e10924607d058004304611a436"`); + await queryRunner.query(`DROP INDEX "IDX_1039988afa3bf991185b277fe0"`); + await queryRunner.query(`DROP TABLE "user_group_invite"`); + } + + async down(queryRunner) { + await queryRunner.query(`CREATE TABLE "user_group_invite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_3893884af0d3a5f4d01e7921a97" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_1039988afa3bf991185b277fe0" ON "user_group_invite" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_e10924607d058004304611a436" ON "user_group_invite" ("userGroupId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_78787741f9010886796f2320a4" ON "user_group_invite" ("userId", "userGroupId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9ecaed8c6dc43f3592c229282" ON "user_group_joining" ("userId", "userGroupId") `); + await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_1039988afa3bf991185b277fe03" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_e10924607d058004304611a436a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 261207528..5d4eca072 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", - "lint": "tsc --noEmit && eslint src --ext .ts", + "lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts", "mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", "migrate": "npx typeorm migration:run -d ormconfig.js", "start": "node --experimental-json-modules ./built/index.js", @@ -113,7 +113,6 @@ "unzipper": "0.10.11", "uuid": "8.3.2", "web-push": "3.5.0", - "websocket": "1.0.34", "ws": "8.8.0", "xev": "3.0.2" }, @@ -164,12 +163,12 @@ "@types/tmp": "0.2.3", "@types/uuid": "8.3.4", "@types/web-push": "3.3.2", - "@types/websocket": "1.0.5", "@types/ws": "8.5.3", "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", "cross-env": "7.0.3", "eslint": "^8.29.0", + "eslint-plugin-foundkey-custom-rules": "file:../shared/custom-rules", "eslint-plugin-import": "^2.26.0", "execa": "6.1.0", "form-data": "^4.0.0", diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 6b1fce48c..4e5327641 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -140,7 +140,7 @@ async function connectDb(): Promise { } async function spawnWorkers(clusterLimits: Required): Promise { - const modes = ['web', 'queue']; + const modes = ['web' as const, 'queue' as const]; const cpus = os.cpus().length; for (const mode of modes.filter(mode => clusterLimits[mode] > cpus)) { bootLogger.warn(`configuration warning: cluster limit for ${mode} exceeds number of cores (${cpus})`); diff --git a/packages/backend/src/misc/app-lock.ts b/packages/backend/src/misc/app-lock.ts index 58e302525..25627fd05 100644 --- a/packages/backend/src/misc/app-lock.ts +++ b/packages/backend/src/misc/app-lock.ts @@ -8,10 +8,7 @@ import { SECOND } from '@/const.js'; */ const retryDelay = 100; -const lock: (key: string, timeout?: number) => Promise<() => void> - = redisClient - ? promisify(redisLock(redisClient, retryDelay)) - : async () => () => { }; +const lock: (key: string, timeout?: number) => Promise<() => void> = promisify(redisLock(redisClient, retryDelay)); /** * Get AP Object lock diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts index e563d749e..7091dcd07 100644 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ b/packages/backend/src/misc/check-hit-antenna.ts @@ -22,7 +22,7 @@ export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'No if (note.visibility === 'specified') return false; // skip if the antenna creator is blocked by the note author - const blockings = await blockingCache.fetch(noteUser.id); + 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/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts index 924b44553..ab8c81eef 100644 --- a/packages/backend/src/misc/fetch-meta.ts +++ b/packages/backend/src/misc/fetch-meta.ts @@ -3,7 +3,7 @@ import { db } from '@/db/postgre.js'; import { Meta } from '@/models/entities/meta.js'; import { getFetchInstanceMetadataLock } from '@/misc/app-lock.js'; -let cache: Meta; +let cache: Meta | undefined; /** * Performs the primitive database operation to set the server configuration @@ -57,5 +57,5 @@ export async function fetchMeta(noCache = false): Promise { await getMeta(); - return cache; + return cache!; } diff --git a/packages/backend/src/misc/fetch.ts b/packages/backend/src/misc/fetch.ts index 661716e25..42eb445d9 100644 --- a/packages/backend/src/misc/fetch.ts +++ b/packages/backend/src/misc/fetch.ts @@ -54,7 +54,11 @@ export async function getResponse(args: { url: string, method: string, body?: st signal: controller.signal, }); - if (!res.ok) { + if ( + !res.ok + && + // intended redirect is not an error + !(args.redirect != 'follow' && res.status >= 300 && res.status < 400)) { throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); } diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts index 949ca696a..51be49a67 100644 --- a/packages/backend/src/misc/reaction-lib.ts +++ b/packages/backend/src/misc/reaction-lib.ts @@ -75,7 +75,7 @@ export async function toDbReaction(reaction?: string | null, idnReacterHost?: st const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); if (custom) { const name = custom[1]; - const emoji = await Emojis.findOneBy({ + const emoji = await Emojis.countBy({ host: reacterHost ?? IsNull(), name, }); diff --git a/packages/backend/src/misc/renote.ts b/packages/backend/src/misc/renote.ts index 758dcdd05..cd51cd04a 100644 --- a/packages/backend/src/misc/renote.ts +++ b/packages/backend/src/misc/renote.ts @@ -1,5 +1,11 @@ import { Note } from '@/models/entities/note.js'; export function isPureRenote(note: Note): note is Note & { renoteId: string, text: null, fileIds: null | never[], hasPoll: false } { - return note.renoteId != null && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !note.hasPoll; + return note.renoteId != null + && note.text == null + && ( + note.fileIds == null + || note.fileIds.length === 0 + ) + && !note.hasPoll; } diff --git a/packages/backend/src/models/entities/announcement.ts b/packages/backend/src/models/entities/announcement.ts index beb2f8246..f2088455d 100644 --- a/packages/backend/src/models/entities/announcement.ts +++ b/packages/backend/src/models/entities/announcement.ts @@ -32,12 +32,4 @@ export class Announcement { length: 1024, nullable: true, }) public imageUrl: string | null; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } } diff --git a/packages/backend/src/models/entities/attestation-challenge.ts b/packages/backend/src/models/entities/attestation-challenge.ts index 2a99953ee..421a07fce 100644 --- a/packages/backend/src/models/entities/attestation-challenge.ts +++ b/packages/backend/src/models/entities/attestation-challenge.ts @@ -35,12 +35,4 @@ export class AttestationChallenge { default: false, }) public registrationChallenge: boolean; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } } diff --git a/packages/backend/src/models/entities/note-thread-muting.ts b/packages/backend/src/models/entities/note-thread-muting.ts index 90ed0c9f8..709dcbaa6 100644 --- a/packages/backend/src/models/entities/note-thread-muting.ts +++ b/packages/backend/src/models/entities/note-thread-muting.ts @@ -2,7 +2,6 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ import { noteNotificationTypes } from 'foundkey-js'; import { id } from '../id.js'; import { User } from './user.js'; -import { Note } from './note.js'; @Entity() @Index(['userId', 'threadId'], { unique: true }) diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index 449debcfc..0aee4c90b 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -155,7 +155,14 @@ export class User { }) public isExplorable: boolean; - // アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ + // for local users: + // Indicates a deletion in progress. + // A hard delete of the record will follow after the deletion finishes. + // + // for remote users: + // Indicates the user was deleted by an admin. + // The users' data is not deleted from the database to keep them from reappearing. + // A hard delete of the record may follow if we receive a matching Delete activity. @Column('boolean', { default: false, comment: 'Whether the User is deleted.', diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index 788949d6d..f75207134 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -126,7 +126,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ const file = typeof src === 'object' ? src : await this.findOneBy({ id: src }); if (file == null) return null; - return await this.pack(file); + return await this.pack(file, opts); }, async packMany( diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index fd308dcae..6a796b223 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -5,7 +5,6 @@ import config from '@/config/index.js'; 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, HOUR } from '@/const.js'; import { Cache } from '@/misc/cache.js'; import { db } from '@/db/postgre.js'; @@ -126,92 +125,73 @@ export const UserRepository = db.getRepository(User).extend({ }, async getHasUnreadMessagingMessage(userId: User['id']): Promise { - const mute = await Mutings.findBy({ - muterId: userId, - }); + return await db.query( + `SELECT EXISTS ( + SELECT 1 + FROM "messaging_message" + WHERE + "recipientId" = $1 + AND + NOT "isRead" + AND + "userId" NOT IN ( + SELECT "muteeId" + FROM "muting" + WHERE "muterId" = $1 + ) - const joinings = await UserGroupJoinings.findBy({ userId }); + UNION - const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message') - .where('message.groupId = :groupId', { groupId: j.userGroupId }) - .andWhere('message.userId != :userId', { userId }) - .andWhere('NOT (:userId = ANY(message.reads))', { userId }) - .andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない - .getOne().then(x => x != null))); - - const [withUser, withGroups] = await Promise.all([ - MessagingMessages.count({ - where: { - recipientId: userId, - isRead: false, - ...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}), - }, - take: 1, - }).then(count => count > 0), - groupQs, - ]); - - return withUser || withGroups.some(x => x); + SELECT 1 + FROM "messaging_message" + JOIN "user_group_joining" + ON "messaging_message"."groupId" = "user_group_joining"."userGroupId" + WHERE + "user_group_joining"."userId" = $1 + AND + "messaging_message"."userId" != $1 + AND + NOT $1 = ANY("messaging_message"."reads") + AND + "messaging_message"."createdAt" > "user_group_joining"."createdAt" + ) AS exists`, + [userId] + ).then(res => res[0].exists); }, async getHasUnreadAnnouncement(userId: User['id']): Promise { - const reads = await AnnouncementReads.findBy({ - userId, - }); - - const count = await Announcements.countBy(reads.length > 0 ? { - id: Not(In(reads.map(read => read.announcementId))), - } : {}); - - return count > 0; + return await db.query( + `SELECT EXISTS (SELECT 1 FROM "announcement" WHERE "id" NOT IN (SELECT "announcementId" FROM "announcement_read" WHERE "userId" = $1)) AS exists`, + [userId] + ).then(res => res[0].exists); }, async getHasUnreadAntenna(userId: User['id']): Promise { - const myAntennas = (await getAntennas()).filter(a => a.userId === userId); - - const unread = myAntennas.length > 0 ? await AntennaNotes.findOneBy({ - antennaId: In(myAntennas.map(x => x.id)), - read: false, - }) : null; - - return unread != null; + return await db.query( + `SELECT EXISTS (SELECT 1 FROM "antenna_note" WHERE NOT "read" AND "antennaId" IN (SELECT "id" FROM "antenna" WHERE "userId" = $1)) AS exists`, + [userId] + ).then(res => res[0].exists); }, async getHasUnreadChannel(userId: User['id']): Promise { - const channels = await ChannelFollowings.findBy({ followerId: userId }); - - const unread = channels.length > 0 ? await NoteUnreads.findOneBy({ - userId, - noteChannelId: In(channels.map(x => x.followeeId)), - }) : null; - - return unread != null; + return await db.query( + `SELECT EXISTS (SELECT 1 FROM "note_unread" WHERE "noteChannelId" IN (SELECT "followeeId" FROM "channel_following" WHERE "followerId" = $1)) AS exists`, + [userId] + ).then(res => res[0].exists); }, async getHasUnreadNotification(userId: User['id']): Promise { - const mute = await Mutings.findBy({ - muterId: userId, - }); - const mutedUserIds = mute.map(m => m.muteeId); - - const count = await Notifications.count({ - where: { - notifieeId: userId, - ...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}), - isRead: false, - }, - take: 1, - }); - - return count > 0; + return await db.query( + `SELECT EXISTS (SELECT 1 FROM "notification" WHERE NOT "isRead" AND "notifieeId" = $1 AND "notifierId" NOT IN (SELECT "muteeId" FROM "muting" WHERE "muterId" = $1)) AS exists`, + [userId] + ).then(res => res[0].exists); }, async getHasPendingReceivedFollowRequest(userId: User['id']): Promise { - const count = await FollowRequests.countBy({ - followeeId: userId, - }); - - return count > 0; + return await db.query( + `SELECT EXISTS (SELECT 1 FROM "follow_request" WHERE "followeeId" = $1) AS exists`, + [userId] + ).then(res => res[0].exists); }, getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' { diff --git a/packages/backend/src/prelude/time.ts b/packages/backend/src/prelude/time.ts index 06a06debd..4e2e320b0 100644 --- a/packages/backend/src/prelude/time.ts +++ b/packages/backend/src/prelude/time.ts @@ -4,20 +4,6 @@ const dateTimeIntervals = { 'ms': 1, }; -export function dateUTC(time: number[]): Date { - const d = time.length === 2 ? Date.UTC(time[0], time[1]) - : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) - : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) - : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) - : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) - : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) - : null; - - if (!d) throw new Error('wrong number of arguments'); - - return new Date(d); -} - export function isTimeSame(a: Date, b: Date): boolean { return a.getTime() === b.getTime(); } diff --git a/packages/backend/src/queue/processors/db/delete-account.ts b/packages/backend/src/queue/processors/db/delete-account.ts index 96f60cfe5..84e28f25d 100644 --- a/packages/backend/src/queue/processors/db/delete-account.ts +++ b/packages/backend/src/queue/processors/db/delete-account.ts @@ -83,10 +83,8 @@ export async function deleteAccount(job: Bull.Job): Promise } } - // soft指定されている場合は物理削除しない - if (job.data.soft) { - // nop - } else { + // No physical deletion if soft is specified. + if (!job.data.soft) { await Users.delete(job.data.user.id); } diff --git a/packages/backend/src/queue/processors/db/import-custom-emojis.ts b/packages/backend/src/queue/processors/db/import-custom-emojis.ts index 04d32d230..1d06d5ff8 100644 --- a/packages/backend/src/queue/processors/db/import-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/import-custom-emojis.ts @@ -56,7 +56,7 @@ export async function importCustomEmojis(job: Bull.Job, don name: emojiInfo.name, }); const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true }); - const emoji = await Emojis.insert({ + await Emojis.insert({ id: genId(), updatedAt: new Date(), name: emojiInfo.name, @@ -66,13 +66,13 @@ export async function importCustomEmojis(job: Bull.Job, don originalUrl: driveFile.url, publicUrl: driveFile.webpublicUrl ?? driveFile.url, type: driveFile.webpublicType ?? driveFile.type, - }).then(x => Emojis.findOneByOrFail(x.identifiers[0])); + }); } await db.queryResultCache!.remove(['meta_emojis']); cleanup(); - + logger.succ('Imported'); done(); }); diff --git a/packages/backend/src/queue/processors/system/check-expired.ts b/packages/backend/src/queue/processors/system/check-expired.ts index ba344b712..eeb6149bb 100644 --- a/packages/backend/src/queue/processors/system/check-expired.ts +++ b/packages/backend/src/queue/processors/system/check-expired.ts @@ -2,7 +2,7 @@ import Bull from 'bull'; import { In, LessThan } from 'typeorm'; import { AttestationChallenges, AuthSessions, Mutings, Notifications, PasswordResetRequests, Signins } from '@/models/index.js'; import { publishUserEvent } from '@/services/stream.js'; -import { MINUTE, DAY, MONTH } from '@/const.js'; +import { MINUTE, MONTH } from '@/const.js'; import { queueLogger } from '@/queue/logger.js'; const logger = queueLogger.createSubLogger('check-expired'); diff --git a/packages/backend/src/remote/activitypub/kernel/create/note.ts b/packages/backend/src/remote/activitypub/kernel/create/note.ts index 0b71566ba..892dbb26a 100644 --- a/packages/backend/src/remote/activitypub/kernel/create/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/create/note.ts @@ -4,7 +4,7 @@ import { extractDbHost } from '@/misc/convert-host.js'; import { StatusError } from '@/misc/fetch.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { createNote, fetchNote } from '@/remote/activitypub/models/note.js'; -import { getApId, IObject, ICreate } from '@/remote/activitypub/type.js'; +import { getApId, IObject } from '@/remote/activitypub/type.js'; /** * 投稿作成アクティビティを捌きます diff --git a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts index 9467eb535..ea75a9739 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts @@ -1,7 +1,7 @@ -import { createDeleteAccountJob } from '@/queue/index.js'; import { CacheableRemoteUser } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { apLogger } from '@/remote/activitypub/logger.js'; +import { deleteAccount } from '@/services/delete-account.js'; export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise { apLogger.info(`Deleting the Actor: ${uri}`); @@ -17,14 +17,9 @@ export async function deleteActor(actor: CacheableRemoteUser, uri: string): Prom return 'ok: gone'; } if (user.isDeleted) { - apLogger.info('skip: already deleted'); + // the actual deletion already happened by an admin, just delete the record + await Users.delete(actor.id); + } else { + await deleteAccount(actor); } - - const job = await createDeleteAccountJob(actor); - - await Users.update(actor.id, { - isDeleted: true, - }); - - return `ok: queued ${job.name} ${job.id}`; } diff --git a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts index b4664cb4a..fa4eea44c 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts @@ -12,7 +12,7 @@ export default async (actor: CacheableRemoteUser, activity: IAccept): Promise typeof x === 'string')); - const results = await Promise.all(uris.map(uri => tryResolveNote(uri))); - - quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x); + const uris = unique([quoteUrl, note._misskey_quote, note.quoteUri].filter((x): x is string => typeof x === 'string')); + // check the urls sequentially and abort early to not do unnecessary HTTP requests + // picks the first one that works + for (const uri in uris) { + const res = await tryResolveNote(uri); + if (res.status === 'ok') { + quote = res.res; + break; + } + } if (!quote) { if (results.some(x => x.status === 'temperror')) { throw new Error('quote resolve failed'); diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 0136aeea3..803fd03c9 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -1,5 +1,5 @@ -import { URL } from 'node:url'; import promiseLimit from 'promise-limit'; +import { Not, IsNull } from 'typeorm'; import config from '@/config/index.js'; import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; @@ -40,7 +40,6 @@ const summaryLength = 2048; * @param uri Fetch target URI */ function validateActor(x: IObject): IActor { - if (x == null) { throw new Error('invalid Actor: object is null'); } @@ -56,6 +55,12 @@ function validateActor(x: IObject): IActor { throw new Error('invalid Actor: wrong id'); } + // This check is security critical. + // Without this check, an entry could be inserted into UserPublickey for a local user. + if (extractDbHost(uri) === extractDbHost(config.url)) { + throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); + } + if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { throw new Error('invalid Actor: wrong inbox'); } @@ -85,9 +90,9 @@ function validateActor(x: IObject): IActor { throw new Error('invalid Actor: publicKey.id is not a string'); } - const expectHost = extractDbHost(uri); - const publicKeyIdHost = extractDbHost(x.publicKey.id); - if (publicKeyIdHost !== expectHost) { + // This is a security critical check to not insert or change an entry of + // UserPublickey to point to a local key id. + if (extractDbHost(uri) !== extractDbHost(x.publicKey.id)) { throw new Error('invalid Actor: publicKey.id has different host'); } } @@ -100,13 +105,13 @@ function validateActor(x: IObject): IActor { * * If the target Person is registered in FoundKey, it is returned. */ -export async function fetchPerson(uri: string, resolver: Resolver): Promise { +export async function fetchPerson(uri: string): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); const cached = uriPersonCache.get(uri); if (cached) return cached; - // URIがこのサーバーを指しているならデータベースからフェッチ + // If the URI points to this server, fetch from database. if (uri.startsWith(config.url + '/')) { const id = uri.split('/').pop(); const u = await Users.findOneBy({ id }); @@ -130,10 +135,6 @@ export async function fetchPerson(uri: string, resolver: Resolver): Promise { - if (getApId(value).startsWith(config.url)) { - throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); - } - const object = await resolver.resolve(value) as any; const person = validateActor(object); @@ -277,13 +278,8 @@ export async function createPerson(value: string | IObject, resolver: Resolver): export async function updatePerson(value: IObject | string, resolver: Resolver): Promise { const uri = getApId(value); - // skip local URIs - if (uri.startsWith(config.url)) { - return; - } - // do we already know this user? - const exist = await Users.findOneBy({ uri }) as IRemoteUser; + const exist = await Users.findOneBy({ uri, host: Not(IsNull()) }) as IRemoteUser; if (exist == null) { return; @@ -384,7 +380,7 @@ export async function resolvePerson(uri: string, resolver: Resolver): Promise x.name!); + .map(x => x.name!); const votes = question[multiple ? 'anyOf' : 'oneOf']! - .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0); + .map(x => x.replies && x.replies.totalItems || x._misskey_votes || 0); return { choices, diff --git a/packages/backend/src/remote/activitypub/models/tag.ts b/packages/backend/src/remote/activitypub/models/tag.ts index 964dabad0..182a23765 100644 --- a/packages/backend/src/remote/activitypub/models/tag.ts +++ b/packages/backend/src/remote/activitypub/models/tag.ts @@ -1,5 +1,5 @@ import { toArray } from '@/prelude/array.js'; -import { IObject, isHashtag, IApHashtag } from '../type.js'; +import { IObject, isHashtag, IApHashtag, isLink, ILink } from '../type.js'; export function extractApHashtags(tags: IObject | IObject[] | null | undefined) { if (tags == null) return []; @@ -16,3 +16,34 @@ export function extractApHashtagObjects(tags: IObject | IObject[] | null | undef if (tags == null) return []; return toArray(tags).filter(isHashtag); } + +// implements FEP-e232: Object Links (2022-12-23 version) +export function extractQuoteUrl(tags: IObject | IObject[] | null | undefined): string | null { + if (tags == null) return null; + + // filter out correct links + let quotes: ILink[] = toArray(tags) + .filter(isLink) + .filter(link => + [ + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'application/activity+json' + ].includes(link.mediaType?.toLowerCase()) + ) + .filter(link => + toArray(link.rel) + .some(rel => + [ + 'https://misskey-hub.net/ns#_misskey_quote', + 'http://fedibird.com/ns#quoteUri', + 'https://www.w3.org/ns/activitystreams#quoteUrl', + ].includes(rel) + ) + ); + + if (quotes.length === 0) return null; + + // Deduplicate by href. + // If there is more than one quote, we just pick the first/a random one. + quotes.filter((x, i, arr) => arr.findIndex(y => x.href === y.href) === i)[0].href; +} diff --git a/packages/backend/src/remote/activitypub/perform.ts b/packages/backend/src/remote/activitypub/perform.ts index 37fd2fc12..8622d43df 100644 --- a/packages/backend/src/remote/activitypub/perform.ts +++ b/packages/backend/src/remote/activitypub/perform.ts @@ -16,4 +16,4 @@ export async function perform(actor: CacheableRemoteUser, activity: IObject, res }); } } -}; +} diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts index bf31c2227..37e73d37b 100644 --- a/packages/backend/src/remote/activitypub/renderer/index.ts +++ b/packages/backend/src/remote/activitypub/renderer/index.ts @@ -21,12 +21,14 @@ export const renderActivity = (x: any): IActivity | null => { manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', sensitive: 'as:sensitive', Hashtag: 'as:Hashtag', - quoteUrl: 'as:quoteUrl', // Mastodon toot: 'http://joinmastodon.org/ns#', Emoji: 'toot:Emoji', featured: 'toot:featured', discoverable: 'toot:discoverable', + // Fedibird + fedibird: 'http://fedibird.com/ns#', + quoteUri: 'fedibird:quoteUri', // schema schema: 'http://schema.org#', PropertyValue: 'schema:PropertyValue', diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index 1bcb5eae2..6fd50c158 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -25,9 +25,9 @@ export default async function renderNote(note: Note, dive = true, isTalk = false inReplyToNote = await Notes.findOneBy({ id: note.replyId }); if (inReplyToNote != null) { - const inReplyToUser = await Users.findOneBy({ id: inReplyToNote.userId }); + const inReplyToUserExists = await Users.countBy({ id: inReplyToNote.userId }); - if (inReplyToUser != null) { + if (inReplyToUserExists) { if (inReplyToNote.uri) { inReplyTo = inReplyToNote.uri; } else { @@ -111,6 +111,16 @@ export default async function renderNote(note: Note, dive = true, isTalk = false ...apemojis, ]; + if (quote) { + tag.push({ + type: 'Link', + mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + href: quote, + name: `RE: ${quote}`, + rel: 'https://misskey-hub.net/ns#_misskey_quote', + }); + } + const asPoll = poll ? { type: 'Question', content: await toHtml(text, note.mentions), @@ -141,7 +151,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false mediaType: 'text/x.misskeymarkdown', }, _misskey_quote: quote, - quoteUrl: quote, + quoteUri: quote, published: note.createdAt.toISOString(), to, cc, diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index 7116e8c8e..233bc025f 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -30,14 +30,15 @@ export async function request(user: { id: User['id'] }, url: string, object: any // don't allow redirects on the inbox redirect: 'error', }); -}; +} /** * Get AP object with http-signature * @param user http-signature user * @param url URL to fetch */ -export async function signedGet(url: string, user: { id: User['id'] }): Promise { +export async function signedGet(_url: string, user: { id: User['id'] }): Promise { + let url = _url; const keypair = await getUserKeypair(user.id); for (let redirects = 0; redirects < 3; redirects++) { diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts index de7eb0ed8..23b4ccf88 100644 --- a/packages/backend/src/remote/activitypub/type.ts +++ b/packages/backend/src/remote/activitypub/type.ts @@ -45,7 +45,7 @@ export function getOneApId(value: ApObject): string { /** * Get ActivityStreams Object id */ -export function getApId(value: string | IObject): string { +export function getApId(value: string | Object): string { if (typeof value === 'string') return value; if (typeof value.id === 'string') return value.id; throw new Error('cannot detemine id'); @@ -54,7 +54,7 @@ export function getApId(value: string | IObject): string { /** * Get ActivityStreams Object type */ -export function getApType(value: IObject): string { +export function getApType(value: Object): string { if (typeof value.type === 'string') return value.type; if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; throw new Error('cannot detect type'); @@ -111,7 +111,7 @@ export interface IPost extends IObject { mediaType: string; }; _misskey_quote?: string; - quoteUrl?: string; + quoteUri?: string; _misskey_talk: boolean; } @@ -122,7 +122,7 @@ export interface IQuestion extends IObject { mediaType: string; }; _misskey_quote?: string; - quoteUrl?: string; + quoteUri?: string; oneOf?: IQuestionChoice[]; anyOf?: IQuestionChoice[]; endTime?: Date; @@ -196,24 +196,6 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue => typeof object.name === 'string' && typeof (object as any).value === 'string'; -export interface IApMention extends IObject { - type: 'Mention'; - href: string; -} - -export const isMention = (object: IObject): object is IApMention => - getApType(object) === 'Mention' && - typeof object.href === 'string'; - -export interface IApHashtag extends IObject { - type: 'Hashtag'; - name: string; -} - -export const isHashtag = (object: IObject): object is IApHashtag => - getApType(object) === 'Hashtag' && - typeof object.name === 'string'; - export interface IApEmoji extends IObject { type: 'Emoji'; updated: Date; @@ -293,3 +275,34 @@ export const isLike = (object: IObject): object is ILike => getApType(object) == export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; + +export interface ILink { + href: string; + rel?: string | string[]; + mediaType?: string; + name?: string; +} + +export interface IApMention extends ILink { + type: 'Mention'; +} + +export interface IApHashtag extends ILink { + type: 'Hashtag'; + name: string; +} + +export const isLink = (object: Record): object is ILink => + typeof object.href === 'string' + && ( + object.rel == undefined + || typeof object.rel === 'string' + || (Array.isArray(object.rel) && object.rel.every(x => typeof x === 'string')) + ) + && (object.mediaType == undefined || typeof object.mediaType === 'string'); +export const isMention = (object: Record): object is IApMention => + getApType(object) === 'Mention' && isLink(object); +export const isHashtag = (object: Record): object is IApHashtag => + getApType(object) === 'Hashtag' + && isLink(object) + && typeof object.name === 'string'; diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts index d88b6c899..60e2ab711 100644 --- a/packages/backend/src/server/activitypub/outbox.ts +++ b/packages/backend/src/server/activitypub/outbox.ts @@ -57,7 +57,7 @@ export default async (ctx: Router.RouterContext) => { .where('note.visibility = \'public\'') .orWhere('note.visibility = \'home\''); })) - .andWhere('note.localOnly = FALSE'); + .andWhere('NOT note.localOnly'); const notes = await query.take(limit).getMany(); diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts index 7ae1c09b0..59a1c9538 100644 --- a/packages/backend/src/server/api/common/signup.ts +++ b/packages/backend/src/server/api/common/signup.ts @@ -70,7 +70,7 @@ export async function signup(opts: { // Start transaction await db.transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.findOneBy(User, { + const exist = await transactionalEntityManager.countBy(User, { usernameLower: username.toLowerCase(), host: IsNull(), }); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 6e69b3b11..59c4305c2 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -54,7 +54,6 @@ import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_vacuum from './endpoints/admin/vacuum.js'; -import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -363,7 +362,6 @@ const eps = [ ['admin/unsuspend-user', ep___admin_unsuspendUser], ['admin/update-meta', ep___admin_updateMeta], ['admin/vacuum', ep___admin_vacuum], - ['admin/delete-account', ep___admin_deleteAccount], ['announcements', ep___announcements], ['antennas/create', ep___antennas_create], ['antennas/delete', ep___antennas_delete], diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index 1e12acac4..e2e215ed8 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -93,8 +93,8 @@ export default define(meta, paramDef, async (ps) => { const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId); switch (ps.state) { - case 'resolved': query.andWhere('report.resolved = TRUE'); break; - case 'unresolved': query.andWhere('report.resolved = FALSE'); break; + case 'resolved': query.andWhere('report.resolved'); break; + case 'unresolved': query.andWhere('NOT report.resolved'); break; } switch (ps.reporterOrigin) { diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index 7c0392e3c..4f5ea5568 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -1,15 +1,13 @@ import { Users } from '@/models/index.js'; import { ApiError } from '@/server/api/error.js'; -import { doPostSuspend } from '@/services/suspend-user.js'; -import { publishUserEvent } from '@/services/stream.js'; -import { createDeleteAccountJob } from '@/queue/index.js'; +import { deleteAccount } from '@/services/delete-account.js'; import define from '../../../define.js'; export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireAdmin: true, errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'], } as const; @@ -23,36 +21,19 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); +export default define(meta, paramDef, async (ps) => { + const user = await Users.findOneBy({ + id: ps.userId, + isDeleted: false, + }); if (user == null) { throw new ApiError('NO_SUCH_USER'); } else if (user.isAdmin) { throw new ApiError('IS_ADMIN'); - } else if(user.isModerator) { + } else if (user.isModerator) { throw new ApiError('IS_MODERATOR'); } - if (Users.isLocalUser(user)) { - // 物理削除する前にDelete activityを送信する - await doPostSuspend(user).catch(e => {}); - - createDeleteAccountJob(user, { - soft: false, - }); - } else { - createDeleteAccountJob(user, { - soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する - }); - } - - await Users.update(user.id, { - isDeleted: true, - }); - - if (Users.isLocalUser(user)) { - // Terminate streaming - publishUserEvent(user.id, 'terminate', {}); - } + await deleteAccount(user); }); 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 656d4c553..c2e5ccf24 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts @@ -20,7 +20,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const announcement = await Announcements.findOneBy({ id: ps.id }); if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT'); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 7a5758d75..1ffce9fbe 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -84,6 +84,6 @@ export default define(meta, paramDef, async (ps) => { title: announcement.title, text: announcement.text, imageUrl: announcement.imageUrl, - reads: reads.get(announcement)!, + reads: reads.get(announcement), })); }); 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 6cc94ce3b..341e5636b 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -23,7 +23,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const announcement = await Announcements.findOneBy({ id: ps.id }); if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT'); diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts deleted file mode 100644 index 2d7ef2f23..000000000 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Users } from '@/models/index.js'; -import { deleteAccount } from '@/services/delete-account.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireAdmin: true, - - res: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneByOrFail({ id: ps.userId }); - if (user.isDeleted) { - return; - } - - await deleteAccount(user); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts index 21870a29b..b6b2b7872 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -18,7 +18,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const files = await DriveFiles.findBy({ userId: ps.userId, }); diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts index 4ee5c113b..b36623293 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts @@ -15,6 +15,6 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async () => { createCleanRemoteFilesJob(); }); diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index ba32aac43..5fcda7614 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -39,7 +39,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId); if (ps.userId) { 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 9f36e6f30..f0c3ea7c0 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 @@ -163,7 +163,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const file = ps.fileId ? await DriveFiles.findOneBy({ id: ps.fileId }) : await DriveFiles.findOne({ where: [{ url: ps.url, 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 a13a47376..a098217b4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -37,7 +37,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const emoji = await Emojis.findOneBy({ id: ps.emojiId }); if (emoji == null) throw new ApiError('NO_SUCH_EMOJI'); diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts index b5d7ec673..91a23e90f 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts @@ -18,7 +18,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const files = await DriveFiles.findBy({ userHost: ps.host, }); diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts index 439a802e1..a043d4601 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts @@ -22,7 +22,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const instance = await Instances.findOneBy({ host: toPuny(ps.host) }); if (instance == null) { diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index 5e60e37a1..34d894b34 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -18,7 +18,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const followings = await Followings.findBy({ followerHost: ps.host, }); diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index a810300b5..9b2e86511 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -22,10 +22,10 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const instance = await Instances.findOneBy({ host: toPuny(ps.host) }); +export default define(meta, paramDef, async (ps) => { + const instanceExists = await Instances.countBy({ host: toPuny(ps.host) }); - if (instance == null) { + if (!instanceExists) { throw new ApiError('NO_SUCH_OBJECT'); } diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 57e60f1c6..ba3159112 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -256,7 +256,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async () => { const instance = await fetchMeta(true); return { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts index 646e2e7cc..b89360720 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -39,7 +39,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async () => { const jobs = await deliverQueue.getJobs(['delayed']); const res = [] as [string, number][]; diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts index a8a2d3d42..274313f8a 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -39,7 +39,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async () => { const jobs = await inboxQueue.getJobs(['delayed']); const res = [] as [string, number][]; diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index 988b5a5e3..7a0d6bdb7 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -38,7 +38,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async () => { const deliverJobCounts = await deliverQueue.getJobCounts(); const inboxJobCounts = await inboxQueue.getJobCounts(); const dbJobCounts = await dbQueue.getJobCounts(); 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 5fbce5411..76a89ef3d 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -49,7 +49,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async (ps) => { try { if (new URL(ps.inbox).protocol !== 'https:') throw new ApiError('INVALID_URL', 'https only'); } catch (e) { diff --git a/packages/backend/src/server/api/endpoints/admin/relays/list.ts b/packages/backend/src/server/api/endpoints/admin/relays/list.ts index c90e9875f..2bc42f74a 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/list.ts @@ -46,6 +46,6 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async () => { return await listRelay(); }); diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts index efd7b9a82..5dcf241b1 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts @@ -17,6 +17,6 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async (ps) => { return await removeRelay(ps.inbox); }); diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 14889e4de..254d76a89 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -26,13 +26,11 @@ export const paramDef = { offset: { type: 'integer', default: 0 }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] }, state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' }, - origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, + origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, username: { type: 'string', nullable: true, default: null }, hostname: { type: 'string', - nullable: true, - default: null, - description: 'The local host is represented with `null`.', + description: "To represent the local host, use `origin: 'local'` instead.", }, }, required: [], @@ -43,13 +41,13 @@ export default define(meta, paramDef, async (ps, me) => { const query = Users.createQueryBuilder('user'); switch (ps.state) { - case 'available': query.where('user.isSuspended = FALSE'); break; - case 'admin': query.where('user.isAdmin = TRUE'); break; - case 'moderator': query.where('user.isModerator = TRUE'); break; - case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break; + case 'available': query.where('NOT user.isSuspended'); break; + case 'admin': query.where('user.isAdmin'); break; + case 'moderator': query.where('user.isModerator'); break; + case 'adminOrModerator': query.where('user.isAdmin OR user.isModerator'); break; case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 5 * DAY) }); break; - case 'silenced': query.where('user.isSilenced = TRUE'); break; - case 'suspended': query.where('user.isSuspended = TRUE'); break; + case 'silenced': query.where('user.isSilenced'); break; + case 'suspended': query.where('user.isSuspended'); break; } switch (ps.origin) { diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 473fa220e..9527f7edf 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -50,9 +50,9 @@ export default define(meta, paramDef, async (ps, me) => { } (async () => { - await doPostSuspend(user).catch(e => {}); - await unFollowAll(user).catch(e => {}); - await readAllNotify(user).catch(e => {}); + await doPostSuspend(user).catch(() => {}); + await unFollowAll(user).catch(() => {}); + await readAllNotify(user).catch(() => {}); })(); }); diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 7770e1220..414f71d31 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -6,7 +6,7 @@ import { extractDbHost } from '@/misc/convert-host.js'; import { Users, Notes } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; import { CacheableLocalUser, User } from '@/models/entities/user.js'; -import { isActor, isPost, getApId } from '@/remote/activitypub/type.js'; +import { isActor, isPost } from '@/remote/activitypub/type.js'; import { SchemaType } from '@/misc/schema.js'; import { HOUR } from '@/const.js'; import { shouldBlockInstance } from '@/misc/should-block-instance.js'; diff --git a/packages/backend/src/server/api/endpoints/auth/deny.ts b/packages/backend/src/server/api/endpoints/auth/deny.ts index b3bb4ab8c..2c0f7abc8 100644 --- a/packages/backend/src/server/api/endpoints/auth/deny.ts +++ b/packages/backend/src/server/api/endpoints/auth/deny.ts @@ -27,7 +27,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async (ps) => { const result = await AuthSessions.delete({ token: ps.token, }); diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 7f45ad522..4a9a2db4a 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already blocking - const exist = await Blockings.findOneBy({ + const blocked = await Blockings.countBy({ blockerId: blocker.id, blockeeId: blockee.id, }); - if (exist != null) throw new ApiError('ALREADY_BLOCKING'); + if (blocked) 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 5e9ca9368..f2a206280 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not blocking - const exist = await Blockings.findOneBy({ + const exist = await Blockings.countBy({ blockerId: blocker.id, blockeeId: blockee.id, }); - if (exist == null) throw new ApiError('NOT_BLOCKING'); + if (!exist) 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 4962bd3da..1a55cebe4 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -7,6 +7,8 @@ import { ApiError } from '../../error.js'; export const meta = { tags: ['channels'], + description: 'Creates a new channel with the current user as its administrator.', + requireCredential: true, kind: 'write:channels', @@ -32,14 +34,13 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - let banner = null; if (ps.bannerId != null) { - banner = await DriveFiles.findOneBy({ + const bannerExists = await DriveFiles.countBy({ id: ps.bannerId, userId: user.id, }); - if (banner == null) throw new ApiError('NO_SUCH_FILE'); + if (!bannerExists) throw new ApiError('NO_SUCH_FILE'); } const channel = await Channels.insert({ @@ -47,8 +48,8 @@ export default define(meta, paramDef, async (ps, user) => { createdAt: new Date(), userId: user.id, name: ps.name, - description: ps.description || null, - bannerId: banner ? banner.id : null, + description: ps.description, + bannerId: ps.bannerId, } as Channel).then(x => Channels.findOneByOrFail(x.identifiers[0])); return await Channels.pack(channel, user); 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 13b432d13..6317db0f3 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -37,12 +37,12 @@ export default define(meta, paramDef, async (ps, user) => { throw err; }); - const exist = await ClipNotes.findOneBy({ + const exist = await ClipNotes.countBy({ noteId: note.id, clipId: clip.id, }); - if (exist != null) throw new ApiError('ALREADY_CLIPPED'); + if (exist) throw new ApiError('ALREADY_CLIPPED'); await ClipNotes.insert({ id: genId(), diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index ef12d1837..73d20450d 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -26,10 +26,8 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ + return 0 < await DriveFiles.countBy({ md5: ps.md5, userId: user.id, }); - - return file != null; }); diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index b0b2280f8..b9f4865fa 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -36,7 +36,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const query = Instances.createQueryBuilder('instance'); switch (ps.sort) { @@ -69,17 +69,17 @@ export default define(meta, paramDef, async (ps, me) => { if (typeof ps.notResponding === 'boolean') { if (ps.notResponding) { - query.andWhere('instance.isNotResponding = TRUE'); + query.andWhere('instance.isNotResponding'); } else { - query.andWhere('instance.isNotResponding = FALSE'); + query.andWhere('NOT instance.isNotResponding'); } } if (typeof ps.suspended === 'boolean') { if (ps.suspended) { - query.andWhere('instance.isSuspended = TRUE'); + query.andWhere('instance.isSuspended'); } else { - query.andWhere('instance.isSuspended = FALSE'); + query.andWhere('NOT instance.isSuspended'); } } 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 22923d5df..a293bd586 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -26,7 +26,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const instance = await Instances .findOneBy({ host: toPuny(ps.host) }); diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index 90ec95172..9341024c0 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -49,12 +49,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already following - const exist = await Followings.findOneBy({ + const exist = await Followings.countBy({ followerId: follower.id, followeeId: followee.id, }); - if (exist != null) throw new ApiError('ALREADY_FOLLOWING'); + if (exist) throw new ApiError('ALREADY_FOLLOWING'); try { await create(follower, followee); diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index 72c1ce6c6..40fe718cc 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not following - const exist = await Followings.findOneBy({ + const exist = await Followings.countBy({ followerId: follower.id, followeeId: followee.id, }); - if (exist == null) throw new ApiError('NOT_FOLLOWING'); + if (!exist) 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 437d20ae7..7bb794caa 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not following - const exist = await Followings.findOneBy({ + const exist = await Followings.countBy({ followerId: follower.id, followeeId: followee.id, }); - if (exist == null) throw new ApiError('NOT_FOLLOWING'); + if (!exist) throw new ApiError('NOT_FOLLOWING'); await deleteFollowing(follower, followee); 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 a2c7e6038..230b7bec3 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => { { param: '#/properties/fileIds/items', reason: 'contains invalid file IDs', - } + }, ); } 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 3e0eda503..1525d4dad 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -27,12 +27,12 @@ export default define(meta, paramDef, async (ps, user) => { if (post == null) throw new ApiError('NO_SUCH_POST'); // if already liked - const exist = await GalleryLikes.findOneBy({ + const exist = await GalleryLikes.countBy({ postId: post.id, userId: user.id, }); - if (exist != null) throw new ApiError('ALREADY_LIKED'); + if (exist) throw new ApiError('ALREADY_LIKED'); // Create like await GalleryLikes.insert({ 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 cd911bf47..071229f89 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -54,7 +54,7 @@ export default define(meta, paramDef, async (ps, user) => { { param: '#/properties/fileIds/items', reason: 'contains invalid file IDs', - } + }, ); } diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts index 4277410c8..5f0d9152c 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts @@ -30,7 +30,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const query = Hashtags.createQueryBuilder('tag'); if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0'); diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts index e66661a68..a3d8b5b98 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts @@ -26,7 +26,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async (ps) => { const hashtag = await Hashtags.findOneBy({ name: normalizeForSearch(ps.tag) }); if (hashtag == null) throw new ApiError('NO_SUCH_HASHTAG'); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index 3800290b7..e9fdd4c37 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -1,7 +1,7 @@ import * as speakeasy from 'speakeasy'; import { UserProfiles } from '@/models/index.js'; -import define from '../../../define.js'; import { ApiError } from '@/server/api/error.js'; +import define from '../../../define.js'; export const meta = { requireCredential: true, diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 4a2ccdc79..b0c4790c5 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -72,7 +72,7 @@ export default define(meta, paramDef, async (ps, user) => { const suspendedQuery = Users.createQueryBuilder('users') .select('users.id') - .where('users.isSuspended = TRUE'); + .where('users.isSuspended'); const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId) .andWhere('notification.notifieeId = :meId', { meId: user.id }) 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 6d7e8954c..bbb510f0d 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -25,17 +25,17 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { // Check if announcement exists - const announcement = await Announcements.findOneBy({ id: ps.announcementId }); + const exists = await Announcements.countBy({ id: ps.announcementId }); - if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT'); + if (!exists) throw new ApiError('NO_SUCH_ANNOUNCEMENT'); // Check if already read - const read = await AnnouncementReads.findOneBy({ + const read = await AnnouncementReads.countBy({ announcementId: ps.announcementId, userId: user.id, }); - if (read != null) return; + if (read) return; // Create read await AnnouncementReads.insert({ diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index 2bec1ed17..84bfa6fd3 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -1,8 +1,8 @@ import { comparePassword } from '@/misc/password.js'; import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js'; import { Users, UserProfiles } from '@/models/index.js'; -import generateUserToken from '../../common/generate-native-user-token.js'; import { ApiError } from '@/server/api/error.js'; +import generateUserToken from '../../common/generate-native-user-token.js'; import define from '../../define.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index 26c20cba9..ef85051f8 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -18,9 +18,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const token = await AccessTokens.findOneBy({ id: ps.tokenId }); + const exists = await AccessTokens.countBy({ id: ps.tokenId }); - if (token) { + if (exists) { await AccessTokens.delete({ id: ps.tokenId, userId: user.id, diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts index 88e17a054..b9fea1259 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages.ts @@ -95,12 +95,12 @@ export default define(meta, paramDef, async (ps, user) => { if (recipientGroup == null) throw new ApiError('NO_SUCH_GROUP'); // check joined - const joining = await UserGroupJoinings.findOneBy({ + const joined = await UserGroupJoinings.countBy({ userId: user.id, userGroupId: recipientGroup.id, }); - if (joining == null) throw new ApiError('ACCESS_DENIED', 'You have to join a group to read messages in it.'); + if (!joined) 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 020d596fc..cbca715c0 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts @@ -106,7 +106,7 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check blocking - const block = await Blockings.findOneBy({ + const block = await Blockings.countBy({ blockerId: recipientUser.id, blockeeId: user.id, }); @@ -118,12 +118,12 @@ export default define(meta, paramDef, async (ps, user) => { if (recipientGroup == null) throw new ApiError('NO_SUCH_GROUP'); // check joined - const joining = await UserGroupJoinings.findOneBy({ + const joined = await UserGroupJoinings.countBy({ userId: user.id, userGroupId: recipientGroup.id, }); - if (joining == null) throw new ApiError('ACCESS_DENIED', 'You have to join a group to send a message in it.'); + if (!joined) throw new ApiError('ACCESS_DENIED', 'You have to join a group to send a message in it.'); } let file = null; diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 03602faac..90d942811 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -235,7 +235,7 @@ export const meta = { }, v2: { - method: 'get' + method: 'get', }, } as const; @@ -253,7 +253,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async () => { const instance = await fetchMeta(true); const emojis = await Emojis.find({ diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index 705fe9deb..ab013bade 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -43,12 +43,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already muting - const exist = await Mutings.findOneBy({ + const exist = await Mutings.countBy({ muterId: muter.id, muteeId: mutee.id, }); - if (exist != null) throw new ApiError('ALREADY_MUTING'); + if (exist) throw new ApiError('ALREADY_MUTING'); if (ps.expiresAt && ps.expiresAt <= Date.now()) { return; diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 015b0338e..b00032e27 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -35,7 +35,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps) => { const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) .andWhere('note.visibility = \'public\'') - .andWhere('note.localOnly = FALSE') + .andWhere('NOT note.localOnly') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -65,7 +65,7 @@ export default define(meta, paramDef, async (ps) => { } if (ps.poll !== undefined) { - query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE'); + query.andWhere((ps.poll ? '' : 'NOT') + 'note.hasPoll'); } // TODO diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 18eec6b89..1b272e9dc 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -169,11 +169,11 @@ export default define(meta, paramDef, async (ps, user) => { // Check blocking if (renote.userId !== user.id) { - const block = await Blockings.findOneBy({ + const blocked = await Blockings.countBy({ blockerId: renote.userId, blockeeId: user.id, }); - if (block) throw new ApiError('BLOCKED', 'Blocked by author of note to be renoted.'); + if (blocked) throw new ApiError('BLOCKED', 'Blocked by author of note to be renoted.'); } } @@ -194,11 +194,11 @@ export default define(meta, paramDef, async (ps, user) => { // Check blocking if (reply.userId !== user.id) { - const block = await Blockings.findOneBy({ + const blocked = await Blockings.countBy({ blockerId: reply.userId, blockeeId: user.id, }); - if (block) throw new ApiError('BLOCKED', 'Blocked by author of replied to note.'); + if (blocked) throw new ApiError('BLOCKED', 'Blocked by author of replied to note.'); } } 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 ffcb9b9e2..4ae1f0830 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -31,12 +31,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // if already favorited - const exist = await NoteFavorites.findOneBy({ + const exist = await NoteFavorites.countBy({ noteId: note.id, userId: user.id, }); - if (exist != null) throw new ApiError('ALREADY_FAVORITED'); + if (exist) throw new ApiError('ALREADY_FAVORITED'); // Create favorite await NoteFavorites.insert({ diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 1382de6e5..112bc39a1 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -21,7 +21,7 @@ export const meta = { v2: { method: 'get', - } + }, } as const; export const paramDef = { 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 2e13fb432..f53d1788c 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -98,7 +98,7 @@ export default define(meta, paramDef, async (ps, user) => { if (ps.excludeNsfw) { query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); + query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive")'); } } //#endregion 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 6648e996b..4ffdcc4c4 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -47,11 +47,11 @@ export default define(meta, paramDef, async (ps, user) => { // Check blocking if (note.userId !== user.id) { - const block = await Blockings.findOneBy({ + const blocked = await Blockings.countBy({ blockerId: note.userId, blockeeId: user.id, }); - if (block) throw new ApiError('BLOCKED'); + if (blocked) throw new ApiError('BLOCKED'); } const poll = await Polls.findOneByOrFail({ noteId: note.id }); @@ -99,7 +99,7 @@ export default define(meta, paramDef, async (ps, user) => { }); // check if this thread and notification type is muted - const threadMuted = await NoteThreadMutings.findOneBy({ + const threadMuted = await NoteThreadMutings.countBy({ userId: note.userId, threadId: note.threadId || note.id, mutingNotificationTypes: ArrayOverlap(['pollVote']), diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 03fe4bd42..6fd0e8f16 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -51,7 +51,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { // check note visibility - const note = await getNote(ps.noteId, user).catch(err => { + await getNote(ps.noteId, user).catch(err => { 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-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index ef9f6c39c..4f4711491 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -127,9 +127,9 @@ export default define(meta, paramDef, async (ps, me) => { if (ps.poll != null) { if (ps.poll) { - query.andWhere('note.hasPoll = TRUE'); + query.andWhere('note.hasPoll'); } else { - query.andWhere('note.hasPoll = FALSE'); + query.andWhere('NOT note.hasPoll'); } } diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 0d7ce97c1..1c4989354 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -3,7 +3,6 @@ import fetch from 'node-fetch'; import config from '@/config/index.js'; import { getAgentByUrl } from '@/misc/fetch.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Notes } from '@/models/index.js'; import { TranslationService } from '@/models/entities/meta.js'; import { ApiError } from '../../error.js'; import { getNote } from '../../common/getters.js'; diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 19b0d99ed..a3f8f3f3a 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -27,12 +27,12 @@ export default define(meta, paramDef, async (ps, user) => { if (page == null) throw new ApiError('NO_SUCH_PAGE'); // if already liked - const exist = await PageLikes.findOneBy({ + const exist = await PageLikes.countBy({ pageId: page.id, userId: user.id, }); - if (exist != null) throw new ApiError('ALREADY_LIKED'); + if (exist) throw new ApiError('ALREADY_LIKED'); // Create like await PageLikes.insert({ 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 9320c4e2c..461312969 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -38,12 +38,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already muting - const exist = await RenoteMutings.findOneBy({ + const exist = await RenoteMutings.countBy({ muterId: muter.id, muteeId: mutee.id, }); - if (exist != null) throw new ApiError('ALREADY_MUTING'); + if (exist) throw new ApiError('ALREADY_MUTING'); // Create mute await RenoteMutings.insert({ diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index ce26e28ee..b8f969bd5 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -17,7 +17,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async () => { if (process.env.NODE_ENV !== 'test') throw new ApiError('ACCESS_DENIED'); await resetDb(); diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index c46fcb6fd..d45b20945 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -28,7 +28,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async (ps) => { const req = await PasswordResetRequests.findOneBy({ token: ps.token, }); diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index 437f8874f..5ae858b99 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -40,7 +40,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { // if already subscribed - const exist = await SwSubscriptions.findOneBy({ + const exist = await SwSubscriptions.countBy({ userId: user.id, endpoint: ps.endpoint, auth: ps.auth, @@ -49,7 +49,7 @@ export default define(meta, paramDef, async (ps, user) => { const instance = await fetchMeta(true); - if (exist != null) { + if (exist) { return { state: 'already-subscribed' as const, key: instance.swPublicKey, diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index e451c3664..45708883d 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -41,12 +41,12 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, me) => { const query = Users.createQueryBuilder('user'); - query.where('user.isExplorable = TRUE'); + query.where('user.isExplorable'); switch (ps.state) { - case 'admin': query.andWhere('user.isAdmin = TRUE'); break; - case 'moderator': query.andWhere('user.isModerator = TRUE'); break; - case 'adminOrModerator': query.andWhere('user.isAdmin = TRUE OR user.isModerator = TRUE'); break; + case 'admin': query.andWhere('user.isAdmin'); break; + case 'moderator': query.andWhere('user.isModerator'); break; + case 'adminOrModerator': query.andWhere('user.isAdmin OR user.isModerator'); break; case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 5 * DAY) }); break; } diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 09fdf27c2..0d3706d7e 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -30,7 +30,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async (ps) => { const query = makePaginationQuery(Clips.createQueryBuilder('clip'), ps.sinceId, ps.untilId) .andWhere('clip.userId = :userId', { userId: ps.userId }) .andWhere('clip.isPublic = true'); diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index e395c3f79..15ecf42fe 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -71,11 +71,11 @@ export default define(meta, paramDef, async (ps, me) => { if (me == null) { throw new ApiError('ACCESS_DENIED'); } else if (me.id !== user.id) { - const following = await Followings.findOneBy({ + const following = await Followings.countBy({ followeeId: user.id, followerId: me.id, }); - if (following == null) { + if (!following) { 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 51ad75a77..85dccccb7 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -71,11 +71,11 @@ export default define(meta, paramDef, async (ps, me) => { if (me == null) { throw new ApiError('ACCESS_DENIED'); } else if (me.id !== user.id) { - const following = await Followings.findOneBy({ + const following = await Followings.countBy({ followeeId: user.id, followerId: me.id, }); - if (following == null) { + if (!following) { throw new ApiError('ACCESS_DENIED'); } } 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 7a792a49b..e8214e815 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -43,14 +43,14 @@ export default define(meta, paramDef, async (ps, me) => { throw e; }); - const joining = await UserGroupJoinings.findOneBy({ + const joined = await UserGroupJoinings.countBy({ userGroupId: userGroup.id, userId: user.id, }); - if (joining) throw new ApiError('ALREADY_ADDED'); + if (joined) throw new ApiError('ALREADY_ADDED'); - const existInvitation = await UserGroupInvitations.findOneBy({ + const existInvitation = await UserGroupInvitations.countBy({ 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 eddd933c2..2aa5abb5b 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts @@ -37,12 +37,12 @@ export default define(meta, paramDef, async (ps, me) => { if (userGroup == null) throw new ApiError('NO_SUCH_GROUP'); - const joining = await UserGroupJoinings.findOneBy({ + const joined = await UserGroupJoinings.countBy({ userId: me.id, userGroupId: userGroup.id, }); - if (joining == null && userGroup.userId !== me.id) { + if (!joined && userGroup.userId !== me.id) { throw new ApiError('NO_SUCH_GROUP'); } 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 b6607e4f8..b9020fbcc 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts @@ -46,12 +46,12 @@ export default define(meta, paramDef, async (ps, me) => { throw e; }); - const joining = await UserGroupJoinings.findOneBy({ + const joined = await UserGroupJoinings.countBy({ userGroupId: userGroup.id, userId: user.id, }); - if (joining == null) throw new ApiError('NO_SUCH_USER', 'The user exists but is not a member of the group.'); + if (!joined) 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/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 7552f3331..86f7aee9a 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -43,14 +43,14 @@ export default define(meta, paramDef, async (ps, me) => { // Check blocking if (user.id !== me.id) { - const block = await Blockings.findOneBy({ + const blocked = await Blockings.countBy({ blockerId: user.id, blockeeId: me.id, }); - if (block) throw new ApiError('BLOCKED'); + if (blocked) throw new ApiError('BLOCKED'); } - const exist = await UserListJoinings.findOneBy({ + const exist = await UserListJoinings.countBy({ userListId: userList.id, userId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index f327e07a5..dddee1907 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -90,7 +90,7 @@ export default define(meta, paramDef, async (ps, me) => { if (ps.excludeNsfw) { query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); + query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive")'); } } diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index b1d28af84..4bfafbdb4 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -30,7 +30,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async (ps) => { const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) .andWhere('page.userId = :userId', { userId: ps.userId }) .andWhere('page.visibility = \'public\''); diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index 62449a3b6..5fe5b47b5 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -36,8 +36,8 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, me) => { const query = Users.createQueryBuilder('user') - .where('user.isLocked = FALSE') - .andWhere('user.isExplorable = TRUE') + .where('NOT user.isLocked') + .andWhere('user.isExplorable') .andWhere('user.host IS NULL') .andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - (7 * DAY)) }) .andWhere('user.id != :meId', { meId: me.id }) diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index cbde7ef79..200e2b925 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -44,7 +44,7 @@ export default define(meta, paramDef, async (ps, me) => { if (ps.host) { const q = Users.createQueryBuilder('user') - .where('user.isSuspended = FALSE') + .where('NOT user.isSuspended') .andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' }); if (ps.username) { @@ -68,7 +68,7 @@ export default define(meta, paramDef, async (ps, me) => { const query = Users.createQueryBuilder('user') .where(`user.id IN (${ followingQuery.getQuery() })`) .andWhere('user.id != :meId', { meId: me.id }) - .andWhere('user.isSuspended = FALSE') + .andWhere('NOT user.isSuspended') .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') @@ -86,7 +86,7 @@ export default define(meta, paramDef, async (ps, me) => { const otherQuery = await Users.createQueryBuilder('user') .where(`user.id NOT IN (${ followingQuery.getQuery() })`) .andWhere('user.id != :meId', { meId: me.id }) - .andWhere('user.isSuspended = FALSE') + .andWhere('user.isSuspended') .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) .andWhere('user.updatedAt IS NOT NULL'); @@ -101,7 +101,7 @@ export default define(meta, paramDef, async (ps, me) => { } } else { users = await Users.createQueryBuilder('user') - .where('user.isSuspended = FALSE') + .where('user.isSuspended') .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) .andWhere('user.updatedAt IS NOT NULL') .orderBy('user.updatedAt', 'DESC') diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index cdd24390a..18fa10401 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -49,7 +49,7 @@ export default define(meta, paramDef, async (ps, me) => { .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold }); })) - .andWhere('user.isSuspended = FALSE'); + .andWhere('NOT user.isSuspended'); if (ps.origin === 'local') { usernameQuery.andWhere('user.host IS NULL'); @@ -64,7 +64,7 @@ export default define(meta, paramDef, async (ps, me) => { .getMany(); } else { const nameQuery = Users.createQueryBuilder('user') - .where(new Brackets(qb => { + .where(new Brackets(qb => { qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' }); // Also search username if it qualifies as username @@ -76,7 +76,7 @@ export default define(meta, paramDef, async (ps, me) => { .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold }); })) - .andWhere('user.isSuspended = FALSE'); + .andWhere('NOT user.isSuspended'); if (ps.origin === 'local') { nameQuery.andWhere('user.host IS NULL'); @@ -103,11 +103,13 @@ export default define(meta, paramDef, async (ps, me) => { const query = Users.createQueryBuilder('user') .where(`user.id IN (${ profQuery.getQuery() })`) + // don't show users twice, but also make sure there is at least one value otherwise this is an invalid query + .andWhere('user.id NOT IN (:...ids)', { ids: users.length === 0 ? [''] : users.map(user => user.id) }) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold }); })) - .andWhere('user.isSuspended = FALSE') + .andWhere('NOT user.isSuspended') .setParameters(profQuery.getParameters()); users = users.concat(await query diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts index f17ff8b31..0f99e56b3 100644 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -110,7 +110,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { +export default define(meta, paramDef, async (ps) => { const user = await Users.findOneBy({ id: ps.userId }); if (user == null) { throw new ApiError('NO_SUCH_USER'); diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index be9f4aef8..c015c7608 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -10,15 +10,16 @@ export class ApiError extends Error { code: keyof errors = 'INTERNAL_ERROR', info?: any | null, ) { + let _info = info, _code = code; if (!(code in errors)) { - info = `Unknown error "${code}" occurred.`; - code = 'INTERNAL_ERROR'; + _code = 'INTERNAL_ERROR'; + _info = `Unknown error "${code}" occurred.`; } - const { message, httpStatusCode } = errors[code]; + const { message, httpStatusCode } = errors[_code]; super(message); - this.code = code; - this.info = info; + this.code = _code; + this.info = _info; this.message = message; this.httpStatusCode = httpStatusCode; } diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index 1aaee70a6..fe0c4450a 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -58,7 +58,7 @@ function uploadWrapper(endpoint: string): KoaMiddleware { apiErr = new ApiError('FILE_TOO_BIG', { maxFileSize: config.maxFileSize || 262144000 }); } apiErr.apply(ctx, endpoint); - } + }, ); }; } diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index 04346c53b..f03da1c4d 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -4,7 +4,8 @@ export function convertSchemaToOpenApiSchema(schema: Schema) { const res: any = schema; if (schema.type === 'object' && schema.properties) { - res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); + res.required = Object.entries(schema.properties) + .flatMap(([k, v]) => v.optional ? [] : [k]); for (const k of Object.keys(schema.properties)) { res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); diff --git a/packages/backend/src/server/api/private/signup-pending.ts b/packages/backend/src/server/api/private/signup-pending.ts index 53fd2058a..09505a895 100644 --- a/packages/backend/src/server/api/private/signup-pending.ts +++ b/packages/backend/src/server/api/private/signup-pending.ts @@ -11,7 +11,7 @@ export default async (ctx: Koa.Context) => { try { const pendingUser = await UserPendings.findOneByOrFail({ code }); - const { account, secret } = await signup({ + const { account } = await signup({ username: pendingUser.username, passwordHash: pendingUser.password, }); diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index a42042d13..343c47ad8 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -2,7 +2,7 @@ import { Note } from '@/models/entities/note.js'; import { Notes } from '@/models/index.js'; import { Packed } from '@/misc/schema.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import Connection from './index.js'; +import { Connection } from './index.js'; /** * Stream channel diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts index 945182ea1..007b459df 100644 --- a/packages/backend/src/server/api/stream/channels/admin.ts +++ b/packages/backend/src/server/api/stream/channels/admin.ts @@ -5,7 +5,7 @@ export default class extends Channel { public static shouldShare = true; public static requireCredential = true; - public async init(params: any) { + public async init() { // Subscribe admin stream this.subscriber.on(`adminStream:${this.user!.id}`, data => { this.send(data); diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts index 140255acd..d7ccab5d8 100644 --- a/packages/backend/src/server/api/stream/channels/drive.ts +++ b/packages/backend/src/server/api/stream/channels/drive.ts @@ -5,7 +5,7 @@ export default class extends Channel { public static shouldShare = true; public static requireCredential = true; - public async init(params: any) { + public async init() { // Subscribe drive stream this.subscriber.on(`driveStream:${this.user!.id}`, data => { this.send(data); 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 6b6f69dfe..cefb80033 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -17,7 +17,7 @@ export default class extends Channel { this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } - public async init(params: any) { + public async init() { const meta = await fetchMeta(); if (meta.disableGlobalTimeline) { if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) 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 e4a5bfd43..1dc81cdbc 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -16,7 +16,7 @@ export default class extends Channel { this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } - public async init(params: any) { + public async init() { // Subscribe events this.subscriber.on('notesStream', this.onNote); } 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 989e70590..e9e60a7c9 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -17,7 +17,7 @@ export default class extends Channel { this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } - public async init(params: any) { + public async init() { const meta = await fetchMeta(); if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return; 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 3aa76e389..84524a154 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -16,7 +16,7 @@ export default class extends Channel { this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); } - public async init(params: any) { + public async init() { const meta = await fetchMeta(); if (meta.disableLocalTimeline) { if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return; diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 70da75e04..f95ca62c8 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -6,7 +6,7 @@ export default class extends Channel { public static shouldShare = true; public static requireCredential = true; - public async init(params: any) { + public async init() { // Subscribe main stream channel this.subscriber.on(`mainStream:${this.user!.id}`, async data => { switch (data.type) { diff --git a/packages/backend/src/server/api/stream/channels/messaging-index.ts b/packages/backend/src/server/api/stream/channels/messaging-index.ts index b930785d2..1498e8649 100644 --- a/packages/backend/src/server/api/stream/channels/messaging-index.ts +++ b/packages/backend/src/server/api/stream/channels/messaging-index.ts @@ -5,7 +5,7 @@ export default class extends Channel { public static shouldShare = true; public static requireCredential = true; - public async init(params: any) { + public async init() { // Subscribe messaging index stream this.subscriber.on(`messagingIndexStream:${this.user!.id}`, data => { this.send(data); diff --git a/packages/backend/src/server/api/stream/channels/messaging.ts b/packages/backend/src/server/api/stream/channels/messaging.ts index ef3ac2ab8..4f9f6a9c4 100644 --- a/packages/backend/src/server/api/stream/channels/messaging.ts +++ b/packages/backend/src/server/api/stream/channels/messaging.ts @@ -31,12 +31,12 @@ export default class extends Channel { // Check joining if (this.groupId) { - const joining = await UserGroupJoinings.findOneBy({ + const joined = await UserGroupJoinings.countBy({ userId: this.user!.id, userGroupId: this.groupId, }); - if (joining == null) { + if (!joined) { return; } } diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts index b67600474..f3dc7d668 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -14,7 +14,7 @@ export default class extends Channel { this.onMessage = this.onMessage.bind(this); } - public async init(params: any) { + public async init() { ev.addListener('queueStats', this.onStats); } diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts index db75a6fa3..7369bbefd 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -14,7 +14,7 @@ export default class extends Channel { this.onMessage = this.onMessage.bind(this); } - public async init(params: any) { + public async init() { ev.addListener('serverStats', this.onStats); } 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 39ee53c2b..0c1a66a5b 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -24,11 +24,11 @@ export default class extends Channel { this.listId = params.listId as string; // Check existence and owner - const list = await UserLists.findOneBy({ + const exists = await UserLists.countBy({ id: this.listId, userId: this.user!.id, }); - if (!list) return; + if (!exists) return; // Subscribe stream this.subscriber.on(`userListStream:${this.listId}`, this.send); diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index f3337fbfe..8ece8e0ff 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import * as websocket from 'websocket'; +import { WebSocket } from 'ws'; import { readNote } from '@/services/note/read.js'; import { User } from '@/models/entities/user.js'; import { Channel as ChannelModel } from '@/models/entities/channel.js'; @@ -13,11 +13,14 @@ import { readNotification } from '../common/read-notification.js'; import channels from './channels/index.js'; import Channel from './channel.js'; import { StreamEventEmitter, StreamMessages } from './types.js'; +import Logger from '@/services/logger.js'; + +const logger = new Logger('streaming'); /** * Main stream connection */ -export default class Connection { +export class Connection { public user?: User; public userProfile?: UserProfile | null; public following: Set = new Set(); @@ -26,29 +29,29 @@ export default class Connection { public blocking: Set = new Set(); // "被"blocking public followingChannels: Set = new Set(); public token?: AccessToken; - private wsConnection: websocket.connection; + private socket: WebSocket; public subscriber: StreamEventEmitter; private channels: Channel[] = []; private subscribingNotes: any = {}; private cachedNotes: Packed<'Note'>[] = []; constructor( - wsConnection: websocket.connection, + socket: WebSocket, subscriber: EventEmitter, user: User | null | undefined, token: AccessToken | null | undefined, ) { - this.wsConnection = wsConnection; + this.socket = socket; this.subscriber = subscriber; if (user) this.user = user; if (token) this.token = token; - this.onWsConnectionMessage = this.onWsConnectionMessage.bind(this); + this.onMessage = this.onMessage.bind(this); this.onUserEvent = this.onUserEvent.bind(this); this.onNoteStreamMessage = this.onNoteStreamMessage.bind(this); this.onBroadcastMessage = this.onBroadcastMessage.bind(this); - this.wsConnection.on('message', this.onWsConnectionMessage); + this.socket.on('message', this.onMessage); this.subscriber.on('broadcast', data => { this.onBroadcastMessage(data); @@ -113,7 +116,7 @@ export default class Connection { break; case 'terminate': - this.wsConnection.close(); + this.socket.close(); this.dispose(); break; @@ -122,40 +125,58 @@ export default class Connection { } } - /** - * クライアントからメッセージ受信時 - */ - private async onWsConnectionMessage(data: websocket.Message) { - if (data.type !== 'utf8') return; - if (data.utf8Data == null) return; + private async onMessage(data: WebSocket.RawData, isRaw: boolean) { + if (isRaw) { + logger.warn('received unexpected raw data from websocket'); + return; + } let obj: Record; try { - obj = JSON.parse(data.utf8Data); - } catch (e) { + obj = JSON.parse(data); + } catch (err) { + logger.error(err); return; } const { type, body } = obj; switch (type) { - case 'readNotification': this.onReadNotification(body); break; - case 'subNote': this.onSubscribeNote(body); break; - case 's': this.onSubscribeNote(body); break; // alias - case 'sr': this.onSubscribeNote(body); this.readNote(body); break; - case 'unsubNote': this.onUnsubscribeNote(body); break; - case 'un': this.onUnsubscribeNote(body); break; // alias - case 'connect': this.onChannelConnectRequested(body); break; - case 'disconnect': this.onChannelDisconnectRequested(body); break; - case 'channel': this.onChannelMessageRequested(body); break; - case 'ch': this.onChannelMessageRequested(body); break; // alias + case 'readNotification': + this.onReadNotification(body); + break; + case 'subNote': case 's': + this.onSubscribeNote(body); + break; + case 'sr': + this.onSubscribeNote(body); + this.readNote(body); + break; + case 'unsubNote': case 'un': + this.onUnsubscribeNote(body); + break; + case 'connect': + this.onChannelConnectRequested(body); + break; + case 'disconnect': + this.onChannelDisconnectRequested(body); + break; + case 'channel': case 'ch': + this.onChannelMessageRequested(body); + break; - // 個々のチャンネルではなくルートレベルでこれらのメッセージを受け取る理由は、 - // クライアントの事情を考慮したとき、入力フォームはノートチャンネルやメッセージのメインコンポーネントとは別 - // なこともあるため、それらのコンポーネントがそれぞれ各チャンネルに接続するようにするのは面倒なため。 - case 'typingOnChannel': this.typingOnChannel(body.channel); break; - case 'typingOnMessaging': this.typingOnMessaging(body); break; + // The reason for receiving these messages at the root level rather than in + // individual channels is that when considering the client's circumstances, the + // input form may be separate from the main components of the note channel or + // message, and it would be cumbersome to have each of those components connect to + // each channel. + case 'typingOnChannel': + this.typingOnChannel(body.channel); + break; + case 'typingOnMessaging': + this.typingOnMessaging(body); + break; } } @@ -259,7 +280,7 @@ export default class Connection { * クライアントにメッセージ送信 */ public sendMessageToWs(type: string, payload: any) { - this.wsConnection.send(JSON.stringify({ + this.socket.send(JSON.stringify({ type, body: payload, })); diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts index dff353c2e..825896b56 100644 --- a/packages/backend/src/server/api/streaming.ts +++ b/packages/backend/src/server/api/streaming.ts @@ -1,71 +1,73 @@ import { EventEmitter } from 'events'; -import { ParsedUrlQuery } from 'querystring'; import * as http from 'node:http'; -import * as websocket from 'websocket'; +import { WebSocketServer } from 'ws'; +import { MINUTE } from '@/const.js'; import { subscriber as redisClient } from '@/db/redis.js'; import { Users } from '@/models/index.js'; -import MainStreamConnection from './stream/index.js'; +import { Connection } from './stream/index.js'; import authenticate from './authenticate.js'; export const initializeStreamingServer = (server: http.Server): void => { // Init websocket server - const ws = new websocket.server({ - httpServer: server, - }); + const ws = new WebSocketServer({ noServer: true }); - ws.on('request', async (request): Promise => { - const q = request.resourceURL.query as ParsedUrlQuery; + server.on('upgrade', async (request, socket, head)=> { + if (!request.url.startsWith('/streaming?')) { + socket.write('HTTP/1.1 400 Bad Request\r\n\r\n', undefined, () => socket.destroy()); + return; + } + const q = new URLSearchParams(request.url.slice(11)); - const [user, app] = await authenticate(request.httpRequest.headers.authorization, q.i) + const [user, app] = await authenticate(request.headers.authorization, q.get('i')) .catch(err => { - request.reject(403, err.message); + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n', undefined, () => socket.destroy()); return []; }); - if (typeof user === 'undefined') { - return; - } + if (typeof user === 'undefined') return; if (user?.isSuspended) { - request.reject(400); + socket.write('HTTP/1.1 403 Forbidden\r\n\r\n', undefined, () => socket.destroy()); return; } - const connection = request.accept(); + ws.handleUpgrade(request, socket, head, (socket) => { + const ev = new EventEmitter(); - const ev = new EventEmitter(); - - async function onRedisMessage(_: string, data: string) { - const parsed = JSON.parse(data); - ev.emit(parsed.channel, parsed.message); - } - - redisClient.on('message', onRedisMessage); - - const main = new MainStreamConnection(connection, ev, user, app); - - const intervalId = user ? setInterval(() => { - Users.update(user.id, { - lastActiveDate: new Date(), - }); - }, 1000 * 60 * 5) : null; - if (user) { - Users.update(user.id, { - lastActiveDate: new Date(), - }); - } - - connection.once('close', () => { - ev.removeAllListeners(); - main.dispose(); - redisClient.off('message', onRedisMessage); - if (intervalId) clearInterval(intervalId); - }); - - connection.on('message', async (data) => { - if (data.type === 'utf8' && data.utf8Data === 'ping') { - connection.send('pong'); + async function onRedisMessage(_: string, data: string) { + const parsed = JSON.parse(data); + ev.emit(parsed.channel, parsed.message); } + + redisClient.on('message', onRedisMessage); + + const main = new Connection(socket, ev, user, app); + + // keep user "online" while a stream is connected + const intervalId = user ? setInterval(() => { + Users.update(user.id, { + lastActiveDate: new Date(), + }); + }, 5 * MINUTE) : null; + if (user) { + Users.update(user.id, { + lastActiveDate: new Date(), + }); + } + socket.once('close', () => { + ev.removeAllListeners(); + main.dispose(); + redisClient.off('message', onRedisMessage); + if (intervalId) clearInterval(intervalId); + }); + + // ping/pong mechanism + // TODO: the websocket protocol already specifies a ping/pong mechanism, why is this necessary? + socket.on('message', async (data) => { + if (data.type === 'utf8' && data.utf8Data === 'ping') { + socket.send('pong'); + } + }); }); }); }; diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 1c33fde10..3776c24f2 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -485,9 +485,6 @@ router.get('/_info_card_', async ctx => { }); }); -const override = (source: string, target: string, depth = 0) => - [, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/'); - router.get('/flush', async ctx => { await ctx.render('flush'); }); diff --git a/packages/backend/src/services/add-note-to-antenna.ts b/packages/backend/src/services/add-note-to-antenna.ts index d502be254..e490eaf11 100644 --- a/packages/backend/src/services/add-note-to-antenna.ts +++ b/packages/backend/src/services/add-note-to-antenna.ts @@ -5,6 +5,7 @@ import { genId } from '@/misc/gen-id.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { publishAntennaStream, publishMainStream } from '@/services/stream.js'; import { User } from '@/models/entities/user.js'; +import { SECOND } from '@/const.js'; export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise { // If it's set to not notify the user, or if it's the user's own post, read it. @@ -45,10 +46,10 @@ export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { // Notify if not read after 2 seconds setTimeout(async () => { - const unread = await AntennaNotes.findOneBy({ antennaId: antenna.id, read: false }); + const unread = await AntennaNotes.countBy({ antennaId: antenna.id, read: false }); if (unread) { publishMainStream(antenna.userId, 'unreadAntenna', antenna); } - }, 2000); + }, 2 * SECOND); } } diff --git a/packages/backend/src/services/chart/charts/per-user-reactions.ts b/packages/backend/src/services/chart/charts/per-user-reactions.ts index 6e9c12f87..27345b99d 100644 --- a/packages/backend/src/services/chart/charts/per-user-reactions.ts +++ b/packages/backend/src/services/chart/charts/per-user-reactions.ts @@ -13,7 +13,7 @@ export default class PerUserReactionsChart extends Chart { super(name, schema, true); } - protected async tickMajor(group: string): Promise>> { + protected async tickMajor(): Promise>> { return {}; } diff --git a/packages/backend/src/services/chart/core.ts b/packages/backend/src/services/chart/core.ts index 025fd87ab..cfc02d11a 100644 --- a/packages/backend/src/services/chart/core.ts +++ b/packages/backend/src/services/chart/core.ts @@ -6,7 +6,7 @@ import * as nestedProperty from 'nested-property'; import { EntitySchema, Repository, LessThan, Between } from 'typeorm'; -import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/prelude/time.js'; +import { isTimeSame, isTimeBefore, subtractTime, addTime } from '@/prelude/time.js'; import { unique } from '@/prelude/array.js'; import { getChartInsertLock } from '@/misc/app-lock.js'; import { db } from '@/db/postgre.js'; @@ -282,10 +282,10 @@ export default abstract class Chart { private async claimCurrentLog(group: string | null, span: 'hour' | 'day'): Promise> { const [y, m, d, h] = Chart.getCurrentDate(); - const current = dateUTC( + const current = new Date(Date.UTC(...( span === 'hour' ? [y, m, d, h] : span === 'day' ? [y, m, d] : - new Error('not happen') as never); + new Error('not happen') as never))); const repository = span === 'hour' ? this.repositoryForHour : @@ -533,7 +533,7 @@ export default abstract class Chart { } public async clean(): Promise { - const current = dateUTC(Chart.getCurrentDate()); + const current = new Date(Date.UTC(...Chart.getCurrentDate())); // more than 1 day and less than 3 days const gt = Chart.dateToTimestamp(current) - (60 * 60 * 24 * 3); @@ -571,11 +571,11 @@ export default abstract class Chart { const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate(); const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never; - const lt = dateUTC([y, m, d, h, _m, _s, _ms]); + const lt = new Date(Date.UTC(y, m, d, h, _m, _s, _ms)); const gt = - span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') : - span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') : + span === 'day' ? subtractTime(cursor ? new Date(Date.UTC(y2, m2, d2, 0)) : new Date(Date.UTC(y, m, d, 0)), amount - 1, 'day') : + span === 'hour' ? subtractTime(cursor ? new Date(Date.UTC(y2, m2, d2, h2)) : new Date(Date.UTC(y, m, d, h)), amount - 1, 'hour') : new Error('not happen') as never; const repository = @@ -632,8 +632,8 @@ export default abstract class Chart { for (let i = (amount - 1); i >= 0; i--) { const current = - span === 'hour' ? subtractTime(dateUTC([y, m, d, h]), i, 'hour') : - span === 'day' ? subtractTime(dateUTC([y, m, d]), i, 'day') : + span === 'hour' ? subtractTime(new Date(Date.UTC(y, m, d, h)), i, 'hour') : + span === 'day' ? subtractTime(new Date(Date.UTC(y, m, d)), i, 'day') : new Error('not happen') as never; const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current)); diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts index bbb4ab356..30499ce4c 100644 --- a/packages/backend/src/services/create-notification.ts +++ b/packages/backend/src/services/create-notification.ts @@ -1,6 +1,6 @@ import { publishMainStream } from '@/services/stream.js'; import { pushNotification } from '@/services/push-notification.js'; -import { Notifications, Mutings, UserProfiles, Users } from '@/models/index.js'; +import { Notifications, Mutings, UserProfiles } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { User } from '@/models/entities/user.js'; import { Notification } from '@/models/entities/notification.js'; diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/services/create-system-user.ts index f965e82d7..8e39b00fb 100644 --- a/packages/backend/src/services/create-system-user.ts +++ b/packages/backend/src/services/create-system-user.ts @@ -22,7 +22,7 @@ export async function createSystemUser(username: string): Promise { // Start transaction await db.transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.findOneBy(User, { + const exist = await transactionalEntityManager.countBy(User, { usernameLower: username.toLowerCase(), host: IsNull(), }); diff --git a/packages/backend/src/services/delete-account.ts b/packages/backend/src/services/delete-account.ts index ed3e381c4..9c8d4c277 100644 --- a/packages/backend/src/services/delete-account.ts +++ b/packages/backend/src/services/delete-account.ts @@ -7,17 +7,21 @@ export async function deleteAccount(user: { id: string; host: string | null; }): Promise { - // Send Delete activity before physical deletion - await doPostSuspend(user).catch(() => {}); - - createDeleteAccountJob(user, { - soft: false, - }); - await Users.update(user.id, { isDeleted: true, }); - // Terminate streaming - publishUserEvent(user.id, 'terminate', {}); + if (Users.isLocalUser(user)) { + // Terminate streaming + publishUserEvent(user.id, 'terminate', {}); + } + + // Send Delete activity before physical deletion + await doPostSuspend(user).catch(() => {}); + + createDeleteAccountJob(user, { + // Deleting remote users is specified as SOFT, because if they are physically deleted + // from the DB completely, they may be reassociated and their accounts may be reinstated. + soft: Users.isLocalUser(user), + }); } diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 18bff576a..81da49df8 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -293,7 +293,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, _type: string async function deleteOldFile(user: IRemoteUser): Promise { const q = DriveFiles.createQueryBuilder('file') .where('file.userId = :userId', { userId: user.id }) - .andWhere('file.isLink = FALSE'); + .andWhere('NOT file.isLink'); if (user.avatarId) { q.andWhere('file.id != :avatarId', { avatarId: user.avatarId }); @@ -384,7 +384,7 @@ export async function addFile({ if (Users.isLocalUser(user)) { throw new Error('no-free-space'); } else { - // (アバターまたはバナーを含まず)最も古いファイルを削除する + // delete oldest file (excluding banner and avatar) deleteOldFile(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser); } } diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts index 624fada32..2fe8993a8 100644 --- a/packages/backend/src/services/drive/delete-file.ts +++ b/packages/backend/src/services/drive/delete-file.ts @@ -64,27 +64,27 @@ export async function deleteFileSync(file: DriveFile, isExpired = false): Promis } async function postProcess(file: DriveFile, isExpired = false): Promise { - // リモートファイル期限切れ削除後は直リンクにする - if (isExpired && file.userHost !== null && file.uri != null) { + // Turn into a direct link after expiring a remote file. + if (isExpired && file.userHost != null && file.uri != null) { + const id = uuid(); DriveFiles.update(file.id, { isLink: true, url: file.uri, thumbnailUrl: null, webpublicUrl: null, storedInternal: false, - // ローカルプロキシ用 - accessKey: uuid(), - thumbnailAccessKey: 'thumbnail-' + uuid(), - webpublicAccessKey: 'webpublic-' + uuid(), + accessKey: id, + thumbnailAccessKey: 'thumbnail-' + id, + webpublicAccessKey: 'webpublic-' + id, }); } else { DriveFiles.delete(file.id); } - // 統計を更新 + // update statistics driveChart.update(file, false); perUserDriveChart.update(file, false); - if (file.userHost !== null) { + if (file.userHost != null) { instanceChart.updateDrive(file, false); } } diff --git a/packages/backend/src/services/drive/internal-storage.ts b/packages/backend/src/services/drive/internal-storage.ts index e76749d6c..ee067844f 100644 --- a/packages/backend/src/services/drive/internal-storage.ts +++ b/packages/backend/src/services/drive/internal-storage.ts @@ -18,7 +18,9 @@ export class InternalStorage { public static saveFromPath(key: string, srcPath: string): string { fs.mkdirSync(InternalStorage.path, { recursive: true }); - fs.copyFileSync(srcPath, InternalStorage.resolvePath(key)); + const target = InternalStorage.resolvePath(key); + fs.copyFileSync(srcPath, target); + fs.chmodSync(target, 0o644); return `${config.url}/files/${key}`; } diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts index a4f546266..7ad85dd99 100644 --- a/packages/backend/src/services/following/create.ts +++ b/packages/backend/src/services/following/create.ts @@ -46,12 +46,12 @@ export async function insertFollowingDoc(followee: { id: User['id']; host: User[ } }); - const req = await FollowRequests.findOneBy({ + const requested = await FollowRequests.countBy({ followeeId: followee.id, followerId: follower.id, }); - if (req) { + if (requested) { await FollowRequests.delete({ followeeId: followee.id, followerId: follower.id, @@ -166,8 +166,8 @@ export default async function(_follower: { id: User['id'] }, _followee: { id: Us if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) { let autoAccept = false; - // 鍵アカウントであっても、既にフォローされていた場合はスルー - const following = await Followings.findOneBy({ + // Even for locked accounts, if they are already following, go through immediately. + const following = await Followings.countBy({ followerId: follower.id, followeeId: followee.id, }); @@ -175,9 +175,9 @@ export default async function(_follower: { id: User['id'] }, _followee: { id: Us autoAccept = true; } - // フォローしているユーザーは自動承認オプション + // handle automatic approval for users that follow you, if enabled if (!autoAccept && (Users.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { - const followed = await Followings.findOneBy({ + const followed = await Followings.countBy({ followerId: followee.id, followeeId: follower.id, }); diff --git a/packages/backend/src/services/following/delete.ts b/packages/backend/src/services/following/delete.ts index b99ff73e2..9a29d1921 100644 --- a/packages/backend/src/services/following/delete.ts +++ b/packages/backend/src/services/following/delete.ts @@ -1,3 +1,4 @@ +import { db } from '@/db/postgre.js'; import { publishMainStream, publishUserEvent } from '@/services/stream.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderFollow from '@/remote/activitypub/renderer/follow.js'; @@ -20,11 +21,15 @@ export default async function(follower: { id: User['id']; host: User['host']; ur }); if (following == null) { - logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); + logger.warn('unfollow requested, but did not follow'); return; } - await Followings.delete(following.id); + await Promise.all([ + Followings.delete(following.id), + // delete notifications that the ex-follower can now no longer see + db.query('DELETE FROM "notification" WHERE "noteId" IS NOT NULL AND "notifieeId" = $1 AND NOT note_visible("noteId", "notifieeId")', [follower.id]), + ]); decrementFollowing(follower, followee); diff --git a/packages/backend/src/services/following/requests/cancel.ts b/packages/backend/src/services/following/requests/cancel.ts index 999ebc64e..4ceac19cb 100644 --- a/packages/backend/src/services/following/requests/cancel.ts +++ b/packages/backend/src/services/following/requests/cancel.ts @@ -16,17 +16,17 @@ export async function cancelFollowRequest(followee: User, follower: User): Promi if (Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); - if (Users.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので + if (Users.isLocalUser(follower)) { deliver(follower, content, followee.inbox); } } - const request = await FollowRequests.findOneBy({ + const requested = await FollowRequests.countBy({ followeeId: followee.id, followerId: follower.id, }); - if (request == null) { + if (!requested) { throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); } diff --git a/packages/backend/src/services/following/requests/create.ts b/packages/backend/src/services/following/requests/create.ts index 81fec5fe4..446f2de40 100644 --- a/packages/backend/src/services/following/requests/create.ts +++ b/packages/backend/src/services/following/requests/create.ts @@ -18,18 +18,18 @@ export async function createFollowRequest(follower: User, followee: User, reques // check blocking const [blocking, blocked] = await Promise.all([ - Blockings.findOneBy({ + Blockings.countBy({ blockerId: follower.id, blockeeId: followee.id, }), - Blockings.findOneBy({ + Blockings.countBy({ blockerId: followee.id, blockeeId: follower.id, }), ]); - if (blocking != null) throw new Error('blocking'); - if (blocked != null) throw new Error('blocked'); + if (blocking) throw new Error('blocking'); + if (blocked) throw new Error('blocked'); const followRequest = await FollowRequests.insert({ id: genId(), diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 7ef25a620..67f5e0d57 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -91,18 +91,18 @@ class NotificationManager { public async deliver(): Promise { for (const x of this.queue) { // check if the sender or thread are muted - const userMuted = await Mutings.findOneBy({ + const userMuted = await Mutings.countBy({ muterId: x.target, muteeId: this.notifier.id, }); - const threadMuted = await NoteThreadMutings.findOneBy({ + const threadMuted = await NoteThreadMutings.countBy({ userId: x.target, threadId: In([ // replies this.note.threadId ?? this.note.id, // renotes - this.note.renoteId ?? undefined + this.note.renoteId ?? undefined, ]), mutingNotificationTypes: ArrayOverlap([x.reason]), }); @@ -376,7 +376,7 @@ export default async (user: { id: User['id']; username: User['username']; host: // 通知 if (data.reply.userHost === null) { - const threadMuted = await NoteThreadMutings.findOneBy({ + const threadMuted = await NoteThreadMutings.countBy({ userId: data.reply.userId, threadId: data.reply.threadId || data.reply.id, }); @@ -623,7 +623,7 @@ async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; }, 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({ + const threadMuted = await NoteThreadMutings.countBy({ userId: u.id, threadId: note.threadId || note.id, }); @@ -656,7 +656,7 @@ async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, } } -function saveReply(reply: Note, note: Note): void { +function saveReply(reply: Note): void { Notes.increment({ id: reply.id }, 'repliesCount', 1); } diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index faa079e95..6075f15b1 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -1,4 +1,4 @@ -import { Brackets, FindOptionsWhere, In, IsNull, Not } from 'typeorm'; +import { 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'; @@ -106,7 +106,7 @@ async function findCascadingNotes(note: Note): Promise { await Promise.all(replies.map(reply => { // only add unique notes - if (cascadingNotes.find((x) => x.id == reply.id) != null) return; + if (cascadingNotes.find((x) => x.id === reply.id) != null) return; cascadingNotes.push(reply); return recursive(reply.id); diff --git a/packages/backend/src/services/note/polls/vote.ts b/packages/backend/src/services/note/polls/vote.ts index 6e22af41f..b86e7107d 100644 --- a/packages/backend/src/services/note/polls/vote.ts +++ b/packages/backend/src/services/note/polls/vote.ts @@ -16,7 +16,7 @@ export async function vote(user: CacheableUser, note: Note, choice: number): Pro // Check blocking if (note.userId !== user.id) { - const block = await Blockings.findOneBy({ + const block = await Blockings.countBy({ blockerId: note.userId, blockeeId: user.id, }); @@ -58,7 +58,7 @@ export async function vote(user: CacheableUser, note: Note, choice: number): Pro }); // check if this thread and notification type is muted - const muted = await NoteThreadMutings.findOneBy({ + const muted = await NoteThreadMutings.countBy({ userId: note.userId, threadId: note.threadId || note.id, mutingNotificationTypes: ArrayOverlap(['pollVote']), diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts index d6e128137..d50761a29 100644 --- a/packages/backend/src/services/note/reaction/create.ts +++ b/packages/backend/src/services/note/reaction/create.ts @@ -18,7 +18,7 @@ import { deleteReaction } from './delete.js'; 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({ + const block = await Blockings.countBy({ blockerId: note.userId, blockeeId: user.id, }); @@ -99,7 +99,7 @@ export async function createReaction(user: { id: User['id']; host: User['host']; }); // check if this thread is muted - const threadMuted = await NoteThreadMutings.findOneBy({ + const threadMuted = await NoteThreadMutings.countBy({ userId: note.userId, threadId: note.threadId || note.id, mutingNotificationTypes: ArrayOverlap(['reaction']), diff --git a/packages/backend/src/services/note/unread.ts b/packages/backend/src/services/note/unread.ts index 829a024ee..4ef257c21 100644 --- a/packages/backend/src/services/note/unread.ts +++ b/packages/backend/src/services/note/unread.ts @@ -3,26 +3,27 @@ import { publishMainStream } from '@/services/stream.js'; import { User } from '@/models/entities/user.js'; import { Mutings, NoteThreadMutings, NoteUnreads } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; +import { SECOND } from '@/const.js'; export async function insertNoteUnread(userId: User['id'], note: Note, params: { - // NOTE: isSpecifiedがtrueならisMentionedは必ずfalse + // NOTE: if isSpecified is true, isMentioned is always false isSpecified: boolean; isMentioned: boolean; }): Promise { - //#region ミュートしているなら無視 - // TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする - const mute = await Mutings.findBy({ + //#region ignore if muted + // TODO: The current design does not apply mutes to channels. + const muted = await Mutings.countBy({ muterId: userId, + muteeId: note.userId, }); - if (mute.map(m => m.muteeId).includes(note.userId)) return; - //#endregion + if (muted) return; - // スレッドミュート - const threadMute = await NoteThreadMutings.findOneBy({ + const threadMuted = await NoteThreadMutings.countBy({ userId, threadId: note.threadId || note.id, }); - if (threadMute) return; + if (threadMuted) return; + //#endregion const unread = { id: genId(), @@ -36,11 +37,11 @@ export async function insertNoteUnread(userId: User['id'], note: Note, params: { await NoteUnreads.insert(unread); - // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する + // Issue the events for unread messages if it hasn't been read after 2 seconds. setTimeout(async () => { - const exist = await NoteUnreads.findOneBy({ id: unread.id }); + const exist = await NoteUnreads.countBy({ id: unread.id }); - if (exist == null) return; + if (!exist) return; if (params.isMentioned) { publishMainStream(userId, 'unreadMention', note.id); @@ -51,5 +52,5 @@ export async function insertNoteUnread(userId: User['id'], note: Note, params: { if (note.channelId) { publishMainStream(userId, 'unreadChannel', note.id); } - }, 2000); + }, 2 * SECOND); } diff --git a/packages/client/package.json b/packages/client/package.json index 5ae21e151..d7cbe0b0f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -15,7 +15,6 @@ "@rollup/pluginutils": "^4.2.1", "@syuilo/aiscript": "0.11.1", "@vitejs/plugin-vue": "^3.1.0", - "abort-controller": "3.0.0", "autobind-decorator": "2.4.0", "autosize": "5.0.1", "blurhash": "1.1.5", @@ -26,12 +25,9 @@ "chartjs-plugin-gradient": "0.5.0", "chartjs-plugin-zoom": "1.2.1", "compare-versions": "4.1.3", - "content-disposition": "0.5.4", "cropperjs": "2.0.0-beta.1", "date-fns": "2.28.0", - "escape-regexp": "0.0.1", "eventemitter3": "4.0.7", - "feed": "4.2.2", "foundkey-js": "workspace:*", "idb-keyval": "6.2.0", "insert-text-at-cursor": "0.3.0", @@ -39,67 +35,39 @@ "katex": "0.16.0", "matter-js": "0.18.0", "mfm-js": "0.22.1", - "mocha": "10.0.0", - "ms": "2.1.3", - "nested-property": "4.0.0", "photoswipe": "5.2.8", "prismjs": "1.28.0", - "private-ip": "2.3.3", - "promise-limit": "2.7.0", - "pug": "3.0.2", "punycode": "2.1.1", - "qrcode": "1.5.1", - "reflect-metadata": "0.1.13", - "rollup": "2.75.7", "sass": "1.53.0", - "seedrandom": "3.0.5", - "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "syuilo-password-strength": "0.0.1", "talisman": "^1.1.4", "textarea-caret": "3.1.0", - "three": "0.142.0", "throttle-debounce": "5.0.0", "tinycolor2": "1.4.2", "tsc-alias": "1.7.0", "tsconfig-paths": "4.1.0", - "twemoji-parser": "14.0.0", "typescript": "^4.9.4", "uuid": "8.3.2", - "v-debounce": "0.1.2", - "vanilla-tilt": "1.7.2", "vite": "3.1.0", "vue": "3.2.45", "vue-prism-editor": "2.0.0-alpha.2", "vuedraggable": "4.0.1", - "wavesurfer.js": "6.0.1", - "websocket": "1.0.34", - "ws": "8.8.0" + "wavesurfer.js": "6.0.1" }, "devDependencies": { - "@types/escape-regexp": "0.0.1", - "@types/glob": "7.2.0", - "@types/gulp": "4.0.9", - "@types/gulp-rename": "2.0.1", - "@types/is-url": "1.2.30", "@types/katex": "0.14.0", "@types/matter-js": "0.17.7", - "@types/mocha": "9.1.1", "@types/punycode": "2.1.0", - "@types/qrcode": "1.5.0", - "@types/seedrandom": "3.0.2", "@types/throttle-debounce": "5.0.0", "@types/tinycolor2": "1.4.3", "@types/uuid": "8.3.4", - "@types/websocket": "1.0.5", - "@types/ws": "8.5.3", "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", "cross-env": "7.0.3", "cypress": "10.3.0", "eslint": "^8.29.0", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-vue": "^9.8.0", - "start-server-and-test": "1.14.0" + "eslint-plugin-vue": "^9.8.0" } } diff --git a/packages/client/src/components/instance-ticker.vue b/packages/client/src/components/instance-ticker.vue index 7704b7d7f..d4dfeb26d 100644 --- a/packages/client/src/components/instance-ticker.vue +++ b/packages/client/src/components/instance-ticker.vue @@ -1,5 +1,5 @@