diff --git a/locales/en-US.yml b/locales/en-US.yml index b54267bd9a45..5ff385b52277 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -909,6 +909,7 @@ followingVisibility: "Visibility of follows" followersVisibility: "Visibility of followers" continueThread: "View thread continuation" deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" +deleteAccountConfirmAndWarn: "This will irreversibly delete your account.\nPlease note that re-logging in after a deletion request will interrupt the deletion of your account.\nProceed?" incorrectPassword: "Incorrect password." voteConfirm: "Confirm your vote for \"{choice}\"?" hide: "Hide" @@ -1813,6 +1814,7 @@ _accountDelete: requestAccountDelete: "Request account deletion" started: "Deletion has been started." inProgress: "Deletion is currently in progress" + dontLogin: "We recommend that you do not log in to your account, as this will interrupt the deletion process." _ad: back: "Back" reduceFrequencyOfThisAd: "Show this ad less" diff --git a/locales/index.d.ts b/locales/index.d.ts index 95b4051a8b64..e01de7ba684f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1796,6 +1796,22 @@ export interface Locale extends ILocale { * モデログ */ "moderationLogs": string; + /** + * アカウント移行使用ログ + */ + "userAccountMoveLogs": string; + /** + * {from} が {to} にアカウントを移行しました + */ + "userAccountMoveLogsTitle": ParameterizedString<"from" | "to">; + /** + * 移行先のアカウントのID + */ + "movedToId": string; + /** + * 移行元のアカウントのID + */ + "moveFromId": string; /** * {n}人が投稿 */ @@ -3660,6 +3676,13 @@ export interface Locale extends ILocale { * アカウントが削除されます。よろしいですか? */ "deleteAccountConfirm": string; + /** + * アカウントが削除されます。 + * 削除リクエスト後に再ログインすると + * アカウントの削除が中断されてしまいますのでご注意ください。 + * よろしいですか? + */ + "deleteAccountConfirmAndWarn": string; /** * パスワードが間違っています。 */ @@ -4368,6 +4391,10 @@ export interface Locale extends ILocale { * このユーザーは新しいアカウントに移行しました: */ "accountMoved": string; + /** + * このユーザーは次のアカウントから移行されました: + */ + "accountMovedFrom": string; /** * このアカウントは移行されています */ @@ -7081,6 +7108,10 @@ export interface Locale extends ILocale { * 削除が進行中 */ "inProgress": string; + /** + * 削除が中断されてしまいますので、アカウントにログインしないことをおすすめします。 + */ + "dontLogin": string; }; "_ad": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6915304b3b3c..baf86e781adb 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -445,6 +445,10 @@ moderation: "モデレーション" moderationNote: "モデレーションノート" addModerationNote: "モデレーションノートを追加する" moderationLogs: "モデログ" +userAccountMoveLogs: "アカウント移行使用ログ" +userAccountMoveLogsTitle: "{from} が {to} にアカウントを移行しました" +movedToId: "移行先のアカウントのID" +moveFromId: "移行元のアカウントのID" nUsersMentioned: "{n}人が投稿" securityKeyAndPasskey: "セキュリティキー・パスキー" securityKey: "セキュリティキー" @@ -911,6 +915,7 @@ followingVisibility: "フォローの公開範囲" followersVisibility: "フォロワーの公開範囲" continueThread: "さらにスレッドを見る" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" +deleteAccountConfirmAndWarn: "アカウントが削除されます。\n削除リクエスト後に再ログインすると\nアカウントの削除が中断されてしまいますのでご注意ください。\nよろしいですか?" incorrectPassword: "パスワードが間違っています。" voteConfirm: "「{choice}」に投票しますか?" hide: "隠す" @@ -1088,6 +1093,7 @@ audioFiles: "音声" dataSaver: "データセーバー" accountMigration: "アカウントの移行" accountMoved: "このユーザーは新しいアカウントに移行しました:" +accountMovedFrom: "このユーザーは次のアカウントから移行されました:" accountMovedShort: "このアカウントは移行されています" operationForbidden: "この操作はできません" forceShowAds: "常に広告を表示する" @@ -1833,6 +1839,7 @@ _accountDelete: requestAccountDelete: "アカウント削除をリクエスト" started: "削除処理が開始されました。" inProgress: "削除が進行中" + dontLogin: "削除が中断されてしまいますので、アカウントにログインしないことをおすすめします。" _ad: back: "戻る" diff --git a/package.json b/package.json index 8e2e5f5d775d..de130d74f0dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.5.0-host.2d", + "version": "2024.5.0-host.2e", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/migration/1724749627479-useraccountmovelogs.js b/packages/backend/migration/1724749627479-useraccountmovelogs.js new file mode 100644 index 000000000000..4b601281bbaa --- /dev/null +++ b/packages/backend/migration/1724749627479-useraccountmovelogs.js @@ -0,0 +1,19 @@ +export class Useraccountmovelogs1724749627479 { + name = 'Useraccountmovelogs1724749627479' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "user_account_move_log" ("id" character varying(32) NOT NULL, "movedToId" character varying(32) NOT NULL, "movedFromId" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_8ffd4ae965a5e3a0fbf4b084212" PRIMARY KEY ("id")); COMMENT ON COLUMN "user_account_move_log"."createdAt" IS 'The created date of the UserIp.'`); + await queryRunner.query(`CREATE INDEX "IDX_d5ee7d4d1b5e7a69d8855ab069" ON "user_account_move_log" ("movedToId") `); + await queryRunner.query(`CREATE INDEX "IDX_82930731d6390e7bb429a1938f" ON "user_account_move_log" ("movedFromId") `); + await queryRunner.query(`ALTER TABLE "user_account_move_log" ADD CONSTRAINT "FK_d5ee7d4d1b5e7a69d8855ab0696" FOREIGN KEY ("movedToId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_account_move_log" ADD CONSTRAINT "FK_82930731d6390e7bb429a1938f8" FOREIGN KEY ("movedFromId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_account_move_log" DROP CONSTRAINT "FK_82930731d6390e7bb429a1938f8"`); + await queryRunner.query(`ALTER TABLE "user_account_move_log" DROP CONSTRAINT "FK_d5ee7d4d1b5e7a69d8855ab0696"`); + await queryRunner.query(`DROP INDEX "public"."IDX_82930731d6390e7bb429a1938f"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d5ee7d4d1b5e7a69d8855ab069"`); + await queryRunner.query(`DROP TABLE "user_account_move_log"`); + } +} diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index fb768bdeb128..ec615fb255f8 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UserAccountMoveLogRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import { IdService } from '@/core/IdService.js'; @@ -48,6 +48,15 @@ export class AccountMoveService { @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.userAccountMoveLogRepository) + private userAccountMoveLogRepository: UserAccountMoveLogRepository, + + @Inject(DI.config) + private config: Config, + private userEntityService: UserEntityService, private idService: IdService, private apPersonService: ApPersonService, @@ -119,6 +128,8 @@ export class AccountMoveService { this.copyBlocking(src, dst), this.copyMutings(src, dst), this.updateLists(src, dst), + this.mergeModerationNote(src, dst), + this.insertAccountMoveLog(src, dst), ]); } catch { /* skip if any error happens */ @@ -256,6 +267,32 @@ export class AccountMoveService { } } + @bindThis + private async mergeModerationNote(src: ThinUser, dst: MiUser): Promise { + const srcprofile = await this.userProfilesRepository.findOneBy({ userId: src.id }); + const dstprofile = await this.userProfilesRepository.findOneBy({ userId: dst.id }); + + if (!srcprofile || !dstprofile) return; + + await this.userProfilesRepository.update({ userId: dst.id }, { + moderationNote: srcprofile.moderationNote + '\n' + dstprofile.moderationNote, + }); + + await this.userProfilesRepository.update({ userId: src.id }, { + moderationNote: srcprofile.moderationNote + '\n' + dstprofile.moderationNote, + }); + } + + @bindThis + private async insertAccountMoveLog(src: ThinUser, dst: MiUser): Promise { + await this.userAccountMoveLogRepository.insert({ + id: this.idService.gen(), + movedToId: dst.id, + movedFromId: src.id, + createdAt: new Date(), + }); + } + @bindThis private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: MiUser): Promise { if (localFollowerIds.length === 0) return; diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 0ac6f7038442..2deec5ba3fa7 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -134,11 +134,14 @@ export class AntennaService implements OnApplicationShutdown { if (antenna.src === 'home') { // TODO } else if (antenna.src === 'list') { - const listUsers = (await this.userListMembershipsRepository.findBy({ - userListId: antenna.userListId!, - })).map(x => x.userId); - - if (!listUsers.includes(note.userId)) return false; + if (antenna.userListId == null) return false; + const exists = await this.userListMembershipsRepository.exists({ + where: { + userListId: antenna.userListId, + userId: note.userId, + }, + }); + if (!exists) return false; } else if (antenna.src === 'users') { const accts = antenna.users.map(x => { const { username, host } = Acct.parse(x); diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 3e5223944c94..eaa828091588 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -101,6 +101,7 @@ import { HashtagEntityService } from './entities/HashtagEntityService.js'; import { InstanceEntityService } from './entities/InstanceEntityService.js'; import { InviteCodeEntityService } from './entities/InviteCodeEntityService.js'; import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js'; +import { UserAccountMoveLogEntityService } from './entities/UserAccountMoveLogEntityService.js'; import { MutingEntityService } from './entities/MutingEntityService.js'; import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js'; import { NoteEntityService } from './entities/NoteEntityService.js'; @@ -242,6 +243,7 @@ const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useEx const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService }; const $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService }; const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService }; +const $UserAccountMoveLogEntityService: Provider = { provide: 'UserAccountMoveLogEntityService', useExisting: UserAccountMoveLogEntityService }; const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService }; const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService }; const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService }; @@ -382,6 +384,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting InstanceEntityService, InviteCodeEntityService, ModerationLogEntityService, + UserAccountMoveLogEntityService, MutingEntityService, RenoteMutingEntityService, NoteEntityService, @@ -518,6 +521,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $InstanceEntityService, $InviteCodeEntityService, $ModerationLogEntityService, + $UserAccountMoveLogEntityService, $MutingEntityService, $RenoteMutingEntityService, $NoteEntityService, @@ -654,6 +658,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting InstanceEntityService, InviteCodeEntityService, ModerationLogEntityService, + UserAccountMoveLogEntityService, MutingEntityService, RenoteMutingEntityService, NoteEntityService, @@ -789,6 +794,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $InstanceEntityService, $InviteCodeEntityService, $ModerationLogEntityService, + $UserAccountMoveLogEntityService, $MutingEntityService, $RenoteMutingEntityService, $NoteEntityService, diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index cfe428c9ea5a..613620840811 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -59,6 +59,7 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { CollapsedQueue } from '@/misc/collapsed-queue.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -150,6 +151,7 @@ type Option = { export class NoteCreateService implements OnApplicationShutdown { private logger: Logger; #shutdownController = new AbortController(); + private updateNotesCountQueue: CollapsedQueue; constructor( @Inject(DI.config) @@ -217,6 +219,7 @@ export class NoteCreateService implements OnApplicationShutdown { private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('note:create'); + this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount); } @bindThis @@ -548,7 +551,7 @@ export class NoteCreateService implements OnApplicationShutdown { // Register host if (this.userEntityService.isRemoteUser(user)) { this.federatedInstanceService.fetch(user.host).then(async i => { - this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); + this.updateNotesCountQueue.enqueue(i.id, 1); if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { this.instanceChart.updateNote(i.host, note, true); } @@ -1093,12 +1096,23 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - public dispose(): void { + private collapseNotesCount(oldValue: number, newValue: number) { + return oldValue + newValue; + } + + @bindThis + private async performUpdateNotesCount(id: MiNote['id'], incrBy: number) { + await this.instancesRepository.increment({ id: id }, 'notesCount', incrBy); + } + + @bindThis + public async dispose(): Promise { this.#shutdownController.abort(); + await this.updateNotesCountQueue.performAllNow(); } @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); + public async onApplicationShutdown(signal?: string | undefined): Promise { + await this.dispose(); } } diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index e20ed06a04c1..adc21781dc41 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -172,6 +172,7 @@ export class SearchService { if (note.text == null && note.cw == null) return; if (!['home', 'public'].includes(note.visibility)) return; + const createdAt = this.idService.parse(note.id).date; if (this.meilisearch) { switch (this.meilisearchIndexScope) { case 'global': @@ -190,7 +191,7 @@ export class SearchService { await this.meilisearchNoteIndex?.addDocuments([{ id: note.id, - createdAt: this.idService.parse(note.id).date.getTime(), + createdAt: createdAt.getTime(), userId: note.userId, userHost: note.userHost, channelId: note.channelId, @@ -202,7 +203,7 @@ export class SearchService { }); } else if (this.elasticsearch) { const body = { - createdAt: this.idService.parse(note.id).date.getTime(), + createdAt: createdAt.getTime(), userId: note.userId, userHost: note.userHost, channelId: note.channelId, @@ -211,11 +212,11 @@ export class SearchService { tags: note.tags, }; await this.elasticsearch.index({ - index: this.elasticsearchNoteIndex + `-${new Date().toISOString().slice(0, 7).replace(/-/g, '')}` as string, + index: `${this.elasticsearchNoteIndex}-${createdAt.toISOString().slice(0, 7).replace(/-/g, '')}`, id: note.id, body: body, }).catch((error) => { - console.error(error); + this.logger.error(error); }); } } @@ -226,6 +227,13 @@ export class SearchService { if (this.meilisearch) { this.meilisearchNoteIndex!.deleteDocument(note.id); + } else if (this.elasticsearch) { + await this.elasticsearch.delete({ + index: `${this.elasticsearchNoteIndex}-${this.idService.parse(note.id).date.toISOString().slice(0, 7).replace(/-/g, '')}`, + id: note.id, + }).catch((error) => { + this.logger.error(error); + }); } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 6a3b5430664d..430ec714a4b2 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -682,10 +682,7 @@ export class ApPersonService implements OnModuleInit { // まずサーバー内で検索して様子見 let dst = await this.fetchPerson(src.movedToUri); - if (dst && this.userEntityService.isLocalUser(dst)) { - // targetがローカルユーザーだった場合データベースから引っ張ってくる - dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as MiLocalUser; - } else if (dst) { + if (dst) { if (movePreventUris.includes(src.movedToUri)) return 'skip: circular move'; // targetを見つけたことがあるならtargetをupdatePersonする @@ -702,13 +699,15 @@ export class ApPersonService implements OnModuleInit { dst = await this.resolvePerson(src.movedToUri); } - if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ??? - if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ??? - if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri'; + const dstUri = this.userEntityService.getUserUri(dst); + const srcUri = this.userEntityService.getUserUri(src); + if (dst.movedToUri === dstUri) return 'skip: movedTo itself (dst)'; // ??? + if (src.movedToUri !== dstUri) return 'skip: missmatch uri'; // ??? + if (dst.movedToUri === srcUri) return 'skip: dst.movedToUri === src.uri'; if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) { return 'skip: dst.alsoKnownAs is empty'; } - if (!dst.alsoKnownAs.includes(src.uri)) { + if (!dst.alsoKnownAs.includes(srcUri)) { return 'skip: alsoKnownAs does not include from.uri'; } diff --git a/packages/backend/src/core/chart/charts/per-user-pv.ts b/packages/backend/src/core/chart/charts/per-user-pv.ts index 40ff9061f648..02baa5ec1d01 100644 --- a/packages/backend/src/core/chart/charts/per-user-pv.ts +++ b/packages/backend/src/core/chart/charts/per-user-pv.ts @@ -54,10 +54,10 @@ export default class PerUserPvChart extends Chart { // eslint-dis } @bindThis - public async getChartUsers(span: 'hour' | 'day', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{ + public async getChartUsers(span: 'hour' | 'day', order: 'ASC' | 'DESC', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{ userId: string; count: number; }[]> { - return await this.getChartPv(span, amount, cursor, limit, offset); + return await this.getChartPv(span, amount, cursor, limit, offset, order); } } diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index 4ecbc5bf04a6..3874c1e144b6 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -727,7 +727,7 @@ export default abstract class Chart { } @bindThis - public async getChartPv(span: 'hour' | 'day', amount: number, cursor: Date | null, limit: number, offset: number): Promise< + public async getChartPv(span: 'hour' | 'day', amount: number, cursor: Date | null, limit: number, offset: number, order: 'ASC' | 'DESC'): Promise< { userId: string, count: number, @@ -749,27 +749,13 @@ export default abstract class Chart { new Error('not happen') as never; // ログ取得 - const logs = await repository.createQueryBuilder() + return await repository.createQueryBuilder() + .select('"group" as "userId", sum("___upv_user" + "___upv_visitor") as "count"') .where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) }) - .orderBy('___pv_visitor + ___upv_visitor + ___pv_user + ___upv_user', 'DESC') - .skip(offset) - .take(limit) - .getMany() as { - ___pv_visitor: number, - ___upv_visitor: number, - ___pv_user: number, - ___upv_user: number, - group: string, - }[]; - const result = [] as { - userId: string, - count: number, - }[]; - for (const row of logs) { - const userId = row.group; - const count = row.___pv_user + row.___upv_user + row.___pv_visitor + row.___upv_visitor; - result.push({ userId, count }); - } - return result; + .groupBy('"userId"') + .orderBy('"count"', order) + .offset(offset) + .limit(limit) + .getRawMany<{ userId: string, count: number }>(); } } diff --git a/packages/backend/src/core/entities/UserAccountMoveLogEntityService.ts b/packages/backend/src/core/entities/UserAccountMoveLogEntityService.ts new file mode 100644 index 000000000000..3b0e9bf166aa --- /dev/null +++ b/packages/backend/src/core/entities/UserAccountMoveLogEntityService.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { MiUserAccountMoveLog, UserAccountMoveLogRepository } from '@/models/_.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { MiUser } from '@/models/User.js'; +import { bindThis } from '@/decorators.js'; +import { Packed } from '@/misc/json-schema.js'; +import { IdService } from '@/core/IdService.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class UserAccountMoveLogEntityService { + constructor( + @Inject(DI.userAccountMoveLogRepository) + private userAccountMoveLogRepository: UserAccountMoveLogRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + ) { + } + + @bindThis + public async pack( + src: MiUserAccountMoveLog['id'] | MiUserAccountMoveLog, + me: { id: MiUser['id'] } | null | undefined, + ) : Promise> { + const log = typeof src === 'object' ? src : await this.userAccountMoveLogRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: log.id, + createdAt: this.idService.parse(log.id).date.toISOString(), + movedFromId: log.movedFromId, + movedFrom: this.userEntityService.pack(log.movedFrom ?? log.movedFromId, me, { + schema: 'UserDetailed', + }), + movedToId: log.movedToId, + movedTo: this.userEntityService.pack(log.movedTo ?? log.movedToId, me, { + schema: 'UserDetailed', + }), + }); + } + + @bindThis + public async packMany( + reports: (MiUserAccountMoveLog['id'] | MiUserAccountMoveLog)[], + me: { id: MiUser['id'] } | null | undefined, + ) : Promise[]> { + return (await Promise.allSettled(reports.map(x => this.pack(x, me)))) + .filter(result => result.status === 'fulfilled') + .map(result => (result as PromiseFulfilledResult>).value); + } +} + diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index fb570d0b4424..a70cce451b16 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -37,6 +37,7 @@ export const DI = { userListMembershipsRepository: Symbol('userListMembershipsRepository'), userNotePiningsRepository: Symbol('userNotePiningsRepository'), userIpsRepository: Symbol('userIpsRepository'), + userAccountMoveLogRepository: Symbol('userAccountMoveLogRepository'), usedUsernamesRepository: Symbol('usedUsernamesRepository'), followingsRepository: Symbol('followingsRepository'), followRequestsRepository: Symbol('followRequestsRepository'), diff --git a/packages/backend/src/misc/collapsed-queue.ts b/packages/backend/src/misc/collapsed-queue.ts new file mode 100644 index 000000000000..dee6df32bd2e --- /dev/null +++ b/packages/backend/src/misc/collapsed-queue.ts @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +type Job = { + value: V; + timer: NodeJS.Timeout; +}; + +export class CollapsedQueue { + private jobs: Map> = new Map(); + + constructor( + private timeout: number, + private collapse: (oldValue: V, newValue: V) => V, + private perform: (key: K, value: V) => Promise, + ) {} + + enqueue(key: K, value: V) { + if (this.jobs.has(key)) { + const old = this.jobs.get(key)!; + const merged = this.collapse(old.value, value); + this.jobs.set(key, { ...old, value: merged }); + } else { + const timer = setTimeout(() => { + const job = this.jobs.get(key)!; + this.jobs.delete(key); + this.perform(key, job.value); + }, this.timeout); + this.jobs.set(key, { value, timer }); + } + } + + async performAllNow() { + const entries = [...this.jobs.entries()]; + this.jobs.clear(); + for (const [_key, job] of entries) { + clearTimeout(job.timer); + } + await Promise.allSettled(entries.map(([key, job]) => this.perform(key, job.value))); + } +} diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 27fd280a8fba..5f169c37dfce 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -37,6 +37,7 @@ import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js'; import { packedUserListMembershipSchema, packedUserListSchema } from '@/models/json-schema/user-list.js'; +import { packedUserAccountMoveLogSchema } from '@/models/json-schema/user-account-move-log.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { @@ -71,6 +72,7 @@ export const refs = { UserList: packedUserListSchema, UserListMembership: packedUserListMembershipSchema, + UserAccountMoveLog: packedUserAccountMoveLogSchema, Ad: packedAdSchema, Announcement: packedAnnouncementSchema, App: packedAppSchema, diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index a61094500284..83eee55ff890 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -73,6 +73,7 @@ import { MiUserProfile, MiUserPublickey, MiUserSecurityKey, + MiUserAccountMoveLog, MiWebhook, MiBubbleGameRecord, MiReversiGame, @@ -200,6 +201,12 @@ const $userListMembershipsRepository: Provider = { inject: [DI.db], }; +const $userAccountMoveLogRepository: Provider = { + provide: DI.userAccountMoveLogRepository, + useFactory: (db: DataSource) => db.getRepository(MiUserAccountMoveLog), + inject: [DI.db], +}; + const $userNotePiningsRepository: Provider = { provide: DI.userNotePiningsRepository, useFactory: (db: DataSource) => db.getRepository(MiUserNotePining), @@ -524,6 +531,7 @@ const $abuseReportResolversRepository: Provider = { $userListsRepository, $userListFavoritesRepository, $userListMembershipsRepository, + $userAccountMoveLogRepository, $userNotePiningsRepository, $userIpsRepository, $usedUsernamesRepository, @@ -596,6 +604,7 @@ const $abuseReportResolversRepository: Provider = { $userListsRepository, $userListFavoritesRepository, $userListMembershipsRepository, + $userAccountMoveLogRepository, $userNotePiningsRepository, $userIpsRepository, $usedUsernamesRepository, diff --git a/packages/backend/src/models/UserAccountMoveLog.ts b/packages/backend/src/models/UserAccountMoveLog.ts new file mode 100644 index 000000000000..6e1869c01e27 --- /dev/null +++ b/packages/backend/src/models/UserAccountMoveLog.ts @@ -0,0 +1,35 @@ +import { Entity, Index, Column, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('user_account_move_log') +export class MiUserAccountMoveLog { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column(id()) + public movedToId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public movedTo: MiUser | null; + + @Index() + @Column(id()) + public movedFromId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public movedFrom: MiUser | null; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserIp.', + default: () => 'CURRENT_TIMESTAMP', + }) + public createdAt: Date; +} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index ca1410c24739..800af98b1f30 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -63,6 +63,7 @@ import { MiUserProfile } from '@/models/UserProfile.js'; import { MiUserPublickey } from '@/models/UserPublickey.js'; import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; import { MiUserMemo } from '@/models/UserMemo.js'; +import { MiUserAccountMoveLog } from '@/models/UserAccountMoveLog.js'; import { MiWebhook } from '@/models/Webhook.js'; import { MiChannel } from '@/models/Channel.js'; import { MiRetentionAggregation } from '@/models/RetentionAggregation.js'; @@ -146,6 +147,7 @@ export { MiUserMemo, MiBubbleGameRecord, MiReversiGame, + MiUserAccountMoveLog, }; export type AbuseReportResolversRepository = Repository; @@ -208,6 +210,7 @@ export type UserPendingsRepository = Repository; export type UserProfilesRepository = Repository; export type UserPublickeysRepository = Repository; export type UserSecurityKeysRepository = Repository; +export type UserAccountMoveLogRepository = Repository; export type WebhooksRepository = Repository; export type ChannelsRepository = Repository; export type RetentionAggregationsRepository = Repository; diff --git a/packages/backend/src/models/json-schema/user-account-move-log.ts b/packages/backend/src/models/json-schema/user-account-move-log.ts new file mode 100644 index 000000000000..ad0f6bc3ca25 --- /dev/null +++ b/packages/backend/src/models/json-schema/user-account-move-log.ts @@ -0,0 +1,36 @@ +export const packedUserAccountMoveLogSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + movedToId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + movedTo: { + type: 'object', + ref: 'UserDetailed', + optional: false, nullable: false, + }, + movedFromId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + movedFrom: { + type: 'object', + ref: 'UserDetailed', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index dfa5d86e127e..58a40ae3c8b4 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -73,6 +73,7 @@ import { MiUserPending } from '@/models/UserPending.js'; import { MiUserProfile } from '@/models/UserProfile.js'; import { MiUserPublickey } from '@/models/UserPublickey.js'; import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; +import { MiUserAccountMoveLog } from '@/models/UserAccountMoveLog.js'; import { MiWebhook } from '@/models/Webhook.js'; import { MiChannel } from '@/models/Channel.js'; import { MiRetentionAggregation } from '@/models/RetentionAggregation.js'; @@ -153,6 +154,7 @@ export const entities = [ MiUserListMembership, MiUserNotePining, MiUserSecurityKey, + MiUserAccountMoveLog, MiUsedUsername, MiFollowing, MiFollowRequest, diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index be800bef49b0..3b9a479a64ce 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -4,7 +4,7 @@ */ import { URL } from 'node:url'; -import { Injectable } from '@nestjs/common'; +import { Injectable, OnApplicationShutdown } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; import type Logger from '@/logger.js'; @@ -26,12 +26,15 @@ import { JsonLdService } from '@/core/activitypub/JsonLdService.js'; import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; import { bindThis } from '@/decorators.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { CollapsedQueue } from '@/misc/collapsed-queue.js'; +import { MiNote } from '@/models/Note.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { InboxJobData } from '../types.js'; @Injectable() -export class InboxProcessorService { +export class InboxProcessorService implements OnApplicationShutdown { private logger: Logger; + private updateInstanceQueue: CollapsedQueue; constructor( private utilityService: UtilityService, @@ -48,6 +51,7 @@ export class InboxProcessorService { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('inbox'); + this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance); } @bindThis @@ -180,10 +184,7 @@ export class InboxProcessorService { // Update stats this.federatedInstanceService.fetch(authUser.user.host).then(i => { - this.federatedInstanceService.update(i.id, { - latestRequestReceivedAt: new Date(), - isNotResponding: false, - }); + this.updateInstanceQueue.enqueue(i.id, new Date()); this.fetchInstanceMetadataService.fetchInstanceMetadata(i); @@ -211,4 +212,27 @@ export class InboxProcessorService { } return 'ok'; } + + @bindThis + public collapseUpdateInstanceJobs(oldValue: Date, newValue: Date) { + return oldValue < newValue ? newValue : oldValue; + } + + @bindThis + public async performUpdateInstance(id: string, value: Date) { + await this.federatedInstanceService.update(id, { + latestRequestReceivedAt: value, + isNotResponding: false, + }); + } + + @bindThis + public async dispose(): Promise { + await this.updateInstanceQueue.performAllNow(); + } + + @bindThis + async onApplicationShutdown(signal?: string) { + await this.dispose(); + } } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index c7670bd0f196..1b669a31c905 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -77,6 +77,7 @@ import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-ab import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; +import * as ep___admin_showUserAccountMoveLogs from './endpoints/admin/show-user-account-move-logs.js'; import * as ep___admin_showUser from './endpoints/admin/show-user.js'; import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; @@ -468,6 +469,7 @@ const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abu const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default }; const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default }; const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default }; +const $admin_showUserAccountMoveLogs: Provider = { provide: 'ep:admin/show-user-account-move-logs', useClass: ep___admin_showUserAccountMoveLogs.default }; const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default }; const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default }; const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default }; @@ -863,6 +865,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_sendEmail, $admin_serverInfo, $admin_showModerationLogs, + $admin_showUserAccountMoveLogs, $admin_showUser, $admin_showUsers, $admin_suspendUser, @@ -1252,6 +1255,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_sendEmail, $admin_serverInfo, $admin_showModerationLogs, + $admin_showUserAccountMoveLogs, $admin_showUser, $admin_showUsers, $admin_suspendUser, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 298796002624..0dc4d9ffb94a 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -77,6 +77,7 @@ import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-ab import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; +import * as ep___admin_showUserAccountMoveLogs from './endpoints/admin/show-user-account-move-logs.js'; import * as ep___admin_showUser from './endpoints/admin/show-user.js'; import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; @@ -466,6 +467,7 @@ const eps = [ ['admin/send-email', ep___admin_sendEmail], ['admin/server-info', ep___admin_serverInfo], ['admin/show-moderation-logs', ep___admin_showModerationLogs], + ['admin/show-user-account-move-logs', ep___admin_showUserAccountMoveLogs], ['admin/show-user', ep___admin_showUser], ['admin/show-users', ep___admin_showUsers], ['admin/suspend-user', ep___admin_suspendUser], diff --git a/packages/backend/src/server/api/endpoints/admin/show-user-account-move-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-user-account-move-logs.ts new file mode 100644 index 000000000000..46e4bd8cc676 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/show-user-account-move-logs.ts @@ -0,0 +1,94 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import type { UserAccountMoveLogRepository } from '@/models/_.js'; +import { UserAccountMoveLogEntityService } from '@/core/entities/UserAccountMoveLogEntityService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'read:admin:show-account-move-log', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + movedToId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + movedTo: { + type: 'object', + optional: false, nullable: false, + ref: 'UserDetailed', + }, + movedFromId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + movedFrom: { + type: 'object', + optional: false, nullable: false, + ref: 'UserDetailed', + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + movedFromId: { type: 'string', format: 'misskey:id', nullable: true }, + movedToId: { type: 'string', format: 'misskey:id', nullable: true }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.userAccountMoveLogRepository) + private userAccountMoveLogRepository: UserAccountMoveLogRepository, + + private userAccountMoveLogEntityService: UserAccountMoveLogEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.userAccountMoveLogRepository.createQueryBuilder('accountMoveLogs'), ps.sinceId, ps.untilId); + + if (ps.movedFromId != null) { + query.andWhere('accountMoveLogs.movedFromId = :movedFromId', { movedFromId: ps.movedFromId }); + } + + if (ps.movedToId != null) { + query.andWhere('accountMoveLogs.movedToId = :movedToId', { movedToId: ps.movedToId }); + } + + const accountMoveLogs = await query.limit(ps.limit).getMany(); + + return await this.userAccountMoveLogEntityService.packMany(accountMoveLogs, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index a424d7a4c7cc..8adbc7b28b36 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -74,7 +74,7 @@ export default class extends Endpoint { // eslint- } const chartUsers: { userId: string; count: number; }[] = []; if (ps.sort?.endsWith('pv')) { - await this.perUserPvChart.getChartUsers('hour', 0, null, ps.limit, ps.offset).then(users => { + await this.perUserPvChart.getChartUsers('hour', ps.sort === '+pv' ? 'DESC' : 'ASC', 0, null, ps.limit, ps.offset).then(users => { chartUsers.push(...users); }); } diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index 6c0774b63426..594e5ab7eef3 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -6,7 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -22,10 +23,11 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; const user = ref(); const props = defineProps<{ - movedTo: string; // user id + movedTo?: string; // user id + movedFrom?: string; // user id }>(); -misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u); +misskeyApi('users/show', { userId: props.movedTo ?? props.movedFrom }).then(u => user.value = u); diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue index 30f12a8fb39b..3fe4f24af57f 100644 --- a/packages/frontend/src/pages/api-console.vue +++ b/packages/frontend/src/pages/api-console.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + @@ -50,6 +50,7 @@ const endpoints = ref([]); const sending = ref(false); const res = ref(''); const withCredential = ref(true); +const endpointAbortController = ref(); misskeyApi('endpoints').then(endpointResponse => { endpoints.value = endpointResponse; @@ -68,7 +69,11 @@ function send() { } function onEndpointChange() { - misskeyApi('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => { + if (endpointAbortController.value) { + endpointAbortController.value.abort(); + } + endpointAbortController.value = new AbortController(); + misskeyApi('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null, endpointAbortController.value.signal).then(resp => { const endpointBody = {}; for (const p of resp.params) { endpointBody[p.name] = diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 6d86a60d9e78..27c9dac2dc2b 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -349,6 +349,7 @@ definePageMetadata(() => ({ > .img { width: 42px; height: 42px; + object-fit: contain; } > .body { @@ -395,6 +396,7 @@ definePageMetadata(() => ({ > .img { width: 32px; height: 32px; + object-fit: contain; } > .body { diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index c5aab5793919..6d6b8d5c92fe 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -114,7 +114,9 @@ async function deleteAccount() { { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.ts.deleteAccountConfirm, + text: i18n.ts.deleteAccountConfirmAndWarn, + okWaitInitiate: 'dialog', + okWaitDuration: 5, }); if (canceled) return; } @@ -129,6 +131,7 @@ async function deleteAccount() { await os.alert({ title: i18n.ts._accountDelete.started, + text: i18n.ts._accountDelete.dontLogin, }); await signout(); diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 42e6bd9fd061..2cd06bb7bf41 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -280,6 +281,7 @@ const memoDraft = ref(props.user.memo); const isEditingMemo = ref(false); const moderationNote = ref(props.user.moderationNote); const editModerationNote = ref(false); +const movedFromLog = ref(null); watch(moderationNote, async () => { await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); @@ -301,6 +303,15 @@ function menu(ev: MouseEvent) { os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup); } +async function fetchMovedFromLog() { + if (!props.user.id) { + movedFromLog.value = null; + return; + } + + movedFromLog.value = await misskeyApi('admin/show-user-account-move-logs', { movedToId: props.user.id }); +} + function parallaxLoop() { parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop); parallax(); @@ -377,6 +388,9 @@ function buildSkebStatus(): string { watch([props.user], () => { memoDraft.value = props.user.memo; fetchSkebStatus(); + if ($i?.isModerator) { + fetchMovedFromLog(); + } }); onMounted(() => { @@ -395,6 +409,9 @@ onMounted(() => { } } fetchSkebStatus(); + if ($i?.isModerator) { + fetchMovedFromLog(); + } nextTick(() => { adjustMemoTextarea(); }); diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 47d78fbac8b0..9dea8a5ffd5f 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -426,6 +426,10 @@ const routes: RouteDef[] = [{ path: '/modlog', name: 'modlog', component: page(() => import('@/pages/admin/modlog.vue')), + }, { + path: '/useraccountmovelog', + name: 'useraccountmovelog', + component: page(() => import('@/pages/admin/useraccountmovelog.vue')), }, { path: '/settings', name: 'settings', diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index acb3973dff06..48b0369cc892 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -337,6 +337,12 @@ type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs'] // @public (undocumented) type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json']; +// @public (undocumented) +type AdminShowUserAccountMoveLogsRequest = operations['admin___show-user-account-move-logs']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminShowUserAccountMoveLogsResponse = operations['admin___show-user-account-move-logs']['responses']['200']['content']['application/json']; + // @public (undocumented) type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json']; @@ -1292,6 +1298,8 @@ declare namespace entities { AdminServerInfoResponse, AdminShowModerationLogsRequest, AdminShowModerationLogsResponse, + AdminShowUserAccountMoveLogsRequest, + AdminShowUserAccountMoveLogsResponse, AdminShowUserRequest, AdminShowUserResponse, AdminShowUsersRequest, @@ -1789,6 +1797,7 @@ declare namespace entities { User, UserList, UserListMembership, + UserAccountMoveLog, Ad, Announcement, App, @@ -2758,7 +2767,7 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content'] function parse(acct: string): Acct; // @public (undocumented) -export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unset-user-mutual-link", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; +export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unset-user-mutual-link", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; @@ -3057,6 +3066,9 @@ function toString_2(acct: Acct): string; // @public (undocumented) type User = components['schemas']['User']; +// @public (undocumented) +type UserAccountMoveLog = components['schemas']['UserAccountMoveLog']; + // @public (undocumented) type UserDetailed = components['schemas']['UserDetailed']; diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 61e726eaf4fd..3ee5c8e0ee48 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.5.0-host.2d", + "version": "2024.5.0-host.2e", "description": "Misskey SDK for JavaScript", "types": "./built/dts/index.d.ts", "exports": { diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 60d8d6ccf709..b58577288e68 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -785,6 +785,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:show-account-move-log* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 59aa29aa7fdc..e4caf32ed860 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -94,6 +94,8 @@ import type { AdminServerInfoResponse, AdminShowModerationLogsRequest, AdminShowModerationLogsResponse, + AdminShowUserAccountMoveLogsRequest, + AdminShowUserAccountMoveLogsResponse, AdminShowUserRequest, AdminShowUserResponse, AdminShowUsersRequest, @@ -655,6 +657,7 @@ export type Endpoints = { 'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse }; 'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse }; 'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse }; + 'admin/show-user-account-move-logs': { req: AdminShowUserAccountMoveLogsRequest; res: AdminShowUserAccountMoveLogsResponse }; 'admin/show-user': { req: AdminShowUserRequest; res: AdminShowUserResponse }; 'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse }; 'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index e9ee6a25382b..26da94dcb18b 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -97,6 +97,8 @@ export type AdminSendEmailRequest = operations['admin___send-email']['requestBod export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json']; export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json']; export type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json']; +export type AdminShowUserAccountMoveLogsRequest = operations['admin___show-user-account-move-logs']['requestBody']['content']['application/json']; +export type AdminShowUserAccountMoveLogsResponse = operations['admin___show-user-account-move-logs']['responses']['200']['content']['application/json']; export type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json']; export type AdminShowUserResponse = operations['admin___show-user']['responses']['200']['content']['application/json']; export type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 6a8eccbf4fd1..e60fe35aaef2 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -9,6 +9,7 @@ export type UserDetailed = components['schemas']['UserDetailed']; export type User = components['schemas']['User']; export type UserList = components['schemas']['UserList']; export type UserListMembership = components['schemas']['UserListMembership']; +export type UserAccountMoveLog = components['schemas']['UserAccountMoveLog']; export type Ad = components['schemas']['Ad']; export type Announcement = components['schemas']['Announcement']; export type App = components['schemas']['App']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 89cee7757bec..131fe65a25d2 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -652,6 +652,15 @@ export type paths = { */ post: operations['admin___show-moderation-logs']; }; + '/admin/show-user-account-move-logs': { + /** + * admin/show-user-account-move-logs + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:show-account-move-log* + */ + post: operations['admin___show-user-account-move-logs']; + }; '/admin/show-user': { /** * admin/show-user @@ -4078,6 +4087,21 @@ export type components = { user: components['schemas']['UserLite']; withReplies: boolean; }; + UserAccountMoveLog: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: id */ + movedToId: string; + movedTo: components['schemas']['UserDetailed']; + /** Format: id */ + movedFromId: string; + movedFrom: components['schemas']['UserDetailed']; + }; Ad: { /** * Format: id @@ -9425,6 +9449,79 @@ export type operations = { }; }; }; + /** + * admin/show-user-account-move-logs + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:show-account-move-log* + */ + 'admin___show-user-account-move-logs': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** Format: misskey:id */ + movedFromId?: string | null; + /** Format: misskey:id */ + movedToId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: id */ + movedToId: string; + movedTo: components['schemas']['UserDetailed']; + /** Format: id */ + movedFromId: string; + movedFrom: components['schemas']['UserDetailed']; + }[]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * admin/show-user * @description No description provided. diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index abc03ca5ca90..5740c906518d 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -57,6 +57,7 @@ export const permissions = [ 'write:admin:send-email', 'read:admin:server-info', 'read:admin:show-moderation-log', + 'read:admin:show-account-move-log', 'read:admin:show-user', 'read:admin:show-users', 'write:admin:suspend-user',