From ac8c66f5abe5b585a5940e172993c8e28f44ad69 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 25 Mar 2022 16:27:41 +0900 Subject: [PATCH] perf(server): refactor and performance improvements --- packages/backend/src/misc/cache.ts | 2 +- packages/backend/src/models/entities/user.ts | 6 +++ .../backend/src/queue/processors/inbox.ts | 8 +++- .../src/remote/activitypub/audience.ts | 16 +++---- .../src/remote/activitypub/db-resolver.ts | 44 ++++++++++++------- .../src/remote/activitypub/deliver-manager.ts | 4 +- .../activitypub/kernel/accept/follow.ts | 4 +- .../remote/activitypub/kernel/accept/index.ts | 4 +- .../remote/activitypub/kernel/add/index.ts | 4 +- .../activitypub/kernel/announce/index.ts | 4 +- .../activitypub/kernel/announce/note.ts | 5 +-- .../remote/activitypub/kernel/block/index.ts | 7 +-- .../remote/activitypub/kernel/create/index.ts | 4 +- .../remote/activitypub/kernel/create/note.ts | 4 +- .../remote/activitypub/kernel/delete/actor.ts | 7 +-- .../remote/activitypub/kernel/delete/index.ts | 4 +- .../remote/activitypub/kernel/delete/note.ts | 4 +- .../remote/activitypub/kernel/flag/index.ts | 4 +- .../src/remote/activitypub/kernel/follow.ts | 4 +- .../src/remote/activitypub/kernel/index.ts | 7 +-- .../src/remote/activitypub/kernel/like.ts | 4 +- .../src/remote/activitypub/kernel/read.ts | 4 +- .../activitypub/kernel/reject/follow.ts | 4 +- .../remote/activitypub/kernel/reject/index.ts | 4 +- .../remote/activitypub/kernel/remove/index.ts | 4 +- .../remote/activitypub/kernel/undo/accept.ts | 4 +- .../activitypub/kernel/undo/announce.ts | 4 +- .../remote/activitypub/kernel/undo/block.ts | 7 +-- .../remote/activitypub/kernel/undo/follow.ts | 4 +- .../remote/activitypub/kernel/undo/index.ts | 6 +-- .../remote/activitypub/kernel/undo/like.ts | 4 +- .../remote/activitypub/kernel/update/index.ts | 4 +- .../src/remote/activitypub/models/image.ts | 8 ++-- .../src/remote/activitypub/models/mention.ts | 6 +-- .../src/remote/activitypub/models/note.ts | 11 ++--- .../src/remote/activitypub/models/person.ts | 18 ++++++-- .../backend/src/remote/activitypub/perform.ts | 14 +++++- .../backend/src/server/activitypub/cache.ts | 4 -- .../src/server/activitypub/followers.ts | 6 +-- .../src/server/activitypub/following.ts | 6 +-- .../backend/src/server/activitypub/outbox.ts | 6 +-- .../backend/src/server/api/authenticate.ts | 25 ++++++----- packages/backend/src/server/api/call.ts | 4 +- packages/backend/src/server/api/define.ts | 22 ++-------- .../api/endpoints/admin/moderators/add.ts | 3 ++ .../api/endpoints/admin/moderators/remove.ts | 2 + .../server/api/endpoints/admin/show-user.ts | 3 +- .../api/endpoints/admin/silence-user.ts | 3 ++ .../api/endpoints/admin/unsilence-user.ts | 3 ++ .../api/endpoints/drive/files/delete.ts | 4 +- .../server/api/endpoints/drive/files/show.ts | 4 +- .../api/endpoints/drive/files/update.ts | 4 +- .../api/endpoints/i/regenerate-token.ts | 11 +++-- .../src/server/api/endpoints/notes/delete.ts | 2 +- .../api/endpoints/notes/global-timeline.ts | 2 +- .../api/endpoints/notes/hybrid-timeline.ts | 4 +- .../api/endpoints/notes/local-timeline.ts | 2 +- .../server/api/endpoints/notes/reactions.ts | 1 - packages/backend/src/server/api/limiter.ts | 4 +- .../backend/src/server/api/stream/types.ts | 5 +++ .../backend/src/services/following/reject.ts | 13 +++++- .../backend/src/services/messages/create.ts | 4 +- packages/backend/src/services/note/create.ts | 6 +-- packages/backend/src/services/note/delete.ts | 4 +- .../backend/src/services/note/polls/vote.ts | 4 +- packages/backend/src/services/relay.ts | 8 +++- packages/backend/src/services/suspend-user.ts | 3 ++ .../backend/src/services/unsuspend-user.ts | 3 ++ packages/backend/src/services/user-cache.ts | 44 +++++++++++++++++++ .../client/src/pages/settings/profile.vue | 2 +- packages/client/src/pages/user-info.vue | 1 + 71 files changed, 289 insertions(+), 189 deletions(-) delete mode 100644 packages/backend/src/server/activitypub/cache.ts create mode 100644 packages/backend/src/services/user-cache.ts diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index e9966b778..9ce5c3e8b 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,5 +1,5 @@ export class Cache { - private cache: Map; + public cache: Map; private lifetime: number; constructor(lifetime: Cache['lifetime']) { diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index 9d5db10eb..c76824c97 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -234,3 +234,9 @@ export interface ILocalUser extends User { export interface IRemoteUser extends User { host: string; } + +export type CacheableLocalUser = ILocalUser; + +export type CacheableRemoteUser = IRemoteUser; + +export type CacheableUser = CacheableLocalUser | CacheableRemoteUser; diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index 1b3f94b70..4fbfdb234 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -15,6 +15,8 @@ import DbResolver from '@/remote/activitypub/db-resolver.js'; import { resolvePerson } from '@/remote/activitypub/models/person.js'; import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js'; import { StatusError } from '@/misc/fetch.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { UserPublickey } from '@/models/entities/user-publickey.js'; const logger = new Logger('inbox'); @@ -42,11 +44,13 @@ export default async (job: Bull.Job): Promise => { return `Old keyId is no longer supported. ${keyIdLower}`; } - // TDOO: キャッシュ const dbResolver = new DbResolver(); // HTTP-Signature keyIdを元にDBから取得 - let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); + let authUser: { + user: CacheableRemoteUser; + key: UserPublickey | null; + } | null = await dbResolver.getAuthUserFromKeyId(signature.keyId); // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得 if (authUser == null) { diff --git a/packages/backend/src/remote/activitypub/audience.ts b/packages/backend/src/remote/activitypub/audience.ts index ba69b11e8..846ccf9c0 100644 --- a/packages/backend/src/remote/activitypub/audience.ts +++ b/packages/backend/src/remote/activitypub/audience.ts @@ -3,26 +3,26 @@ import Resolver from './resolver.js'; import { resolvePerson } from './models/person.js'; import { unique, concat } from '@/prelude/array.js'; import promiseLimit from 'promise-limit'; -import { User, IRemoteUser } from '@/models/entities/user.js'; +import { User, CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js'; type Visibility = 'public' | 'home' | 'followers' | 'specified'; type AudienceInfo = { visibility: Visibility, - mentionedUsers: User[], - visibleUsers: User[], + mentionedUsers: CacheableUser[], + visibleUsers: CacheableUser[], }; -export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { +export async function parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { const toGroups = groupingAudience(getApIds(to), actor); const ccGroups = groupingAudience(getApIds(cc), actor); const others = unique(concat([toGroups.other, ccGroups.other])); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( others.map(id => limit(() => resolvePerson(id, resolver).catch(() => null))) - )).filter((x): x is User => x != null); + )).filter((x): x is CacheableUser => x != null); if (toGroups.public.length > 0) { return { @@ -55,7 +55,7 @@ export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApOb }; } -function groupingAudience(ids: string[], actor: IRemoteUser) { +function groupingAudience(ids: string[], actor: CacheableRemoteUser) { const groups = { public: [] as string[], followers: [] as string[], @@ -85,7 +85,7 @@ function isPublic(id: string) { ].includes(id); } -function isFollowers(id: string, actor: IRemoteUser) { +function isFollowers(id: string, actor: CacheableRemoteUser) { return ( id === (actor.followersUri || `${actor.uri}/followers`) ); diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index 3f16c5f56..3e7d2655f 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -1,12 +1,17 @@ +import escapeRegexp from 'escape-regexp'; import config from '@/config/index.js'; import { Note } from '@/models/entities/note.js'; -import { User, IRemoteUser } from '@/models/entities/user.js'; +import { User, IRemoteUser, CacheableRemoteUser } from '@/models/entities/user.js'; import { UserPublickey } from '@/models/entities/user-publickey.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js'; import { IObject, getApId } from './type.js'; import { resolvePerson } from './models/person.js'; -import escapeRegexp from 'escape-regexp'; +import { Cache } from '@/misc/cache.js'; +import { userByIdCache } from '@/services/user-cache.js'; + +const publicKeyCache = new Cache(Infinity); +const publicKeyByUserIdCache = new Cache(Infinity); export default class DbResolver { constructor() { @@ -75,17 +80,24 @@ export default class DbResolver { /** * AP KeyId => Misskey User and Key */ - public async getAuthUserFromKeyId(keyId: string): Promise { - const key = await UserPublickeys.findOne({ - keyId, - }, { - relations: ['user'], - }); + public async getAuthUserFromKeyId(keyId: string): Promise<{ + user: CacheableRemoteUser; + key: UserPublickey; + } | null> { + const key = await publicKeyCache.fetch(keyId, async () => { + const key = await UserPublickeys.findOne({ + keyId, + }); + + if (key == null) return null; + + return key; + }, key => key != null); if (key == null) return null; return { - user: key.user as IRemoteUser, + user: await userByIdCache.fetch(key.userId, () => Users.findOneOrFail(key.userId)) as CacheableRemoteUser, key, }; } @@ -93,12 +105,15 @@ export default class DbResolver { /** * AP Actor id => Misskey User and Key */ - public async getAuthUserFromApId(uri: string): Promise { - const user = await resolvePerson(uri) as IRemoteUser; + public async getAuthUserFromApId(uri: string): Promise<{ + user: CacheableRemoteUser; + key: UserPublickey | null; + } | null> { + const user = await resolvePerson(uri) as CacheableRemoteUser; if (user == null) return null; - const key = await UserPublickeys.findOne(user.id); + const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOne(user.id).then(x => x || null), v => v != null); // TODO: typeorm 3.0 にしたら.then(x => x || null)は消せる return { user, @@ -125,11 +140,6 @@ export default class DbResolver { } } -export type AuthUser = { - user: IRemoteUser; - key?: UserPublickey; -}; - type UriParseResult = { /** id in DB (local object only) */ id?: string; diff --git a/packages/backend/src/remote/activitypub/deliver-manager.ts b/packages/backend/src/remote/activitypub/deliver-manager.ts index 9c4e3418f..6cbd57af5 100644 --- a/packages/backend/src/remote/activitypub/deliver-manager.ts +++ b/packages/backend/src/remote/activitypub/deliver-manager.ts @@ -112,7 +112,7 @@ export default class DeliverManager { * @param activity Activity * @param from Followee */ -export async function deliverToFollowers(actor: ILocalUser, activity: any) { +export async function deliverToFollowers(actor: { id: ILocalUser['id']; host: null; }, activity: any) { const manager = new DeliverManager(actor, activity); manager.addFollowersRecipe(); await manager.execute(); @@ -123,7 +123,7 @@ export async function deliverToFollowers(actor: ILocalUser, activity: any) { * @param activity Activity * @param to Target user */ -export async function deliverToUser(actor: ILocalUser, activity: any, to: IRemoteUser) { +export async function deliverToUser(actor: { id: ILocalUser['id']; host: null; }, activity: any, to: IRemoteUser) { const manager = new DeliverManager(actor, activity); manager.addDirectRecipe(to); await manager.execute(); diff --git a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts index 393516add..4350ef133 100644 --- a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts @@ -1,10 +1,10 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import accept from '@/services/following/requests/accept.js'; import { IFollow } from '../../type.js'; import DbResolver from '../../db-resolver.js'; import { relayAccepted } from '@/services/relay.js'; -export default async (actor: IRemoteUser, activity: IFollow): Promise => { +export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある const dbResolver = new DbResolver(); diff --git a/packages/backend/src/remote/activitypub/kernel/accept/index.ts b/packages/backend/src/remote/activitypub/kernel/accept/index.ts index 354bd4f6e..78ef75ade 100644 --- a/packages/backend/src/remote/activitypub/kernel/accept/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/accept/index.ts @@ -1,12 +1,12 @@ import Resolver from '../../resolver.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import acceptFollow from './follow.js'; import { IAccept, isFollow, getApType } from '../../type.js'; import { apLogger } from '../../logger.js'; const logger = apLogger; -export default async (actor: IRemoteUser, activity: IAccept): Promise => { +export default async (actor: CacheableRemoteUser, activity: IAccept): Promise => { const uri = activity.id || activity; logger.info(`Accept: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/add/index.ts b/packages/backend/src/remote/activitypub/kernel/add/index.ts index 9a2fac1e7..c813414f9 100644 --- a/packages/backend/src/remote/activitypub/kernel/add/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/add/index.ts @@ -1,9 +1,9 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { IAdd } from '../../type.js'; import { resolveNote } from '../../models/note.js'; import { addPinned } from '@/services/i/pin.js'; -export default async (actor: IRemoteUser, activity: IAdd): Promise => { +export default async (actor: CacheableRemoteUser, activity: IAdd): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/announce/index.ts b/packages/backend/src/remote/activitypub/kernel/announce/index.ts index 7e2e73bdd..ae7e507c9 100644 --- a/packages/backend/src/remote/activitypub/kernel/announce/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/announce/index.ts @@ -1,12 +1,12 @@ import Resolver from '../../resolver.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import announceNote from './note.js'; import { IAnnounce, getApId } from '../../type.js'; import { apLogger } from '../../logger.js'; const logger = apLogger; -export default async (actor: IRemoteUser, activity: IAnnounce): Promise => { +export default async (actor: CacheableRemoteUser, activity: IAnnounce): Promise => { const uri = getApId(activity); logger.info(`Announce: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts index f6068fac7..680749f4d 100644 --- a/packages/backend/src/remote/activitypub/kernel/announce/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/announce/note.ts @@ -1,6 +1,6 @@ import Resolver from '../../resolver.js'; import post from '@/services/note/create.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { IAnnounce, getApId } from '../../type.js'; import { fetchNote, resolveNote } from '../../models/note.js'; import { apLogger } from '../../logger.js'; @@ -15,10 +15,9 @@ const logger = apLogger; /** * アナウンスアクティビティを捌きます */ -export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise { +export default async function(resolver: Resolver, actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise { const uri = getApId(activity); - // アナウンサーが凍結されていたらスキップ if (actor.isSuspended) { return; } diff --git a/packages/backend/src/remote/activitypub/kernel/block/index.ts b/packages/backend/src/remote/activitypub/kernel/block/index.ts index 9e4f1b316..69037fec1 100644 --- a/packages/backend/src/remote/activitypub/kernel/block/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/block/index.ts @@ -1,9 +1,10 @@ import { IBlock } from '../../type.js'; import block from '@/services/blocking/create.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import DbResolver from '../../db-resolver.js'; +import { Users } from '@/models/index.js'; -export default async (actor: IRemoteUser, activity: IBlock): Promise => { +export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず const dbResolver = new DbResolver(); @@ -17,6 +18,6 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise => return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`; } - await block(actor, blockee); + await block(await Users.findOneOrFail(actor.id), blockee); return `ok`; }; diff --git a/packages/backend/src/remote/activitypub/kernel/create/index.ts b/packages/backend/src/remote/activitypub/kernel/create/index.ts index 1187b95ac..c253f9f66 100644 --- a/packages/backend/src/remote/activitypub/kernel/create/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/create/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import createNote from './note.js'; import { ICreate, getApId, isPost, getApType } from '../../type.js'; import { apLogger } from '../../logger.js'; @@ -7,7 +7,7 @@ import { toArray, concat, unique } from '@/prelude/array.js'; const logger = apLogger; -export default async (actor: IRemoteUser, activity: ICreate): Promise => { +export default async (actor: CacheableRemoteUser, activity: ICreate): Promise => { const uri = getApId(activity); logger.info(`Create: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/create/note.ts b/packages/backend/src/remote/activitypub/kernel/create/note.ts index b5c47990a..f8dabe06e 100644 --- a/packages/backend/src/remote/activitypub/kernel/create/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/create/note.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { createNote, fetchNote } from '../../models/note.js'; import { getApId, IObject, ICreate } from '../../type.js'; import { getApLock } from '@/misc/app-lock.js'; @@ -9,7 +9,7 @@ import { StatusError } from '@/misc/fetch.js'; /** * 投稿作成アクティビティを捌きます */ -export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { +export default async function(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { const uri = getApId(note); if (typeof note === 'object') { diff --git a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts index 2f75841e5..53dabb119 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts @@ -1,18 +1,19 @@ import { apLogger } from '../../logger.js'; import { createDeleteAccountJob } from '@/queue/index.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; const logger = apLogger; -export async function deleteActor(actor: IRemoteUser, uri: string): Promise { +export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise { logger.info(`Deleting the Actor: ${uri}`); if (actor.uri !== uri) { return `skip: delete actor ${actor.uri} !== ${uri}`; } - if (actor.isDeleted) { + const user = await Users.findOneOrFail(actor.id); + if (user.isDeleted) { logger.info(`skip: already deleted`); } diff --git a/packages/backend/src/remote/activitypub/kernel/delete/index.ts b/packages/backend/src/remote/activitypub/kernel/delete/index.ts index b6d5e96d0..4c06a9de0 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/index.ts @@ -1,5 +1,5 @@ import deleteNote from './note.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '../../type.js'; import { toSingle } from '@/prelude/array.js'; import { deleteActor } from './actor.js'; @@ -7,7 +7,7 @@ import { deleteActor } from './actor.js'; /** * 削除アクティビティを捌きます */ -export default async (actor: IRemoteUser, activity: IDelete): Promise => { +export default async (actor: CacheableRemoteUser, activity: IDelete): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts index ad5e1a2ed..1f44c3556 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/note.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import deleteNode from '@/services/note/delete.js'; import { apLogger } from '../../logger.js'; import DbResolver from '../../db-resolver.js'; @@ -7,7 +7,7 @@ import { deleteMessage } from '@/services/messages/delete.js'; const logger = apLogger; -export default async function(actor: IRemoteUser, uri: string): Promise { +export default async function(actor: CacheableRemoteUser, uri: string): Promise { logger.info(`Deleting the Note: ${uri}`); const unlock = await getApLock(uri); diff --git a/packages/backend/src/remote/activitypub/kernel/flag/index.ts b/packages/backend/src/remote/activitypub/kernel/flag/index.ts index e80e63278..45c0a6c71 100644 --- a/packages/backend/src/remote/activitypub/kernel/flag/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/flag/index.ts @@ -1,11 +1,11 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import config from '@/config/index.js'; import { IFlag, getApIds } from '../../type.js'; import { AbuseUserReports, Users } from '@/models/index.js'; import { In } from 'typeorm'; import { genId } from '@/misc/gen-id.js'; -export default async (actor: IRemoteUser, activity: IFlag): Promise => { +export default async (actor: CacheableRemoteUser, activity: IFlag): Promise => { // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する const uris = getApIds(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/follow.ts b/packages/backend/src/remote/activitypub/kernel/follow.ts index 49c1a7ee0..a9e92fa22 100644 --- a/packages/backend/src/remote/activitypub/kernel/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/follow.ts @@ -1,9 +1,9 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import follow from '@/services/following/create.js'; import { IFollow } from '../type.js'; import DbResolver from '../db-resolver.js'; -export default async (actor: IRemoteUser, activity: IFollow): Promise => { +export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { const dbResolver = new DbResolver(); const followee = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/index.ts b/packages/backend/src/remote/activitypub/kernel/index.ts index 6aea8e57c..254a12160 100644 --- a/packages/backend/src/remote/activitypub/kernel/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/index.ts @@ -1,5 +1,5 @@ import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import create from './create/index.js'; import performDeleteActivity from './delete/index.js'; import performUpdateActivity from './update/index.js'; @@ -17,8 +17,9 @@ import flag from './flag/index.js'; import { apLogger } from '../logger.js'; import Resolver from '../resolver.js'; import { toArray } from '@/prelude/array.js'; +import { Users } from '@/models/index.js'; -export async function performActivity(actor: IRemoteUser, activity: IObject) { +export async function performActivity(actor: CacheableRemoteUser, activity: IObject) { if (isCollectionOrOrderedCollection(activity)) { const resolver = new Resolver(); for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { @@ -36,7 +37,7 @@ export async function performActivity(actor: IRemoteUser, activity: IObject) { } } -async function performOneActivity(actor: IRemoteUser, activity: IObject): Promise { +async function performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise { if (actor.isSuspended) return; if (isCreate(activity)) { diff --git a/packages/backend/src/remote/activitypub/kernel/like.ts b/packages/backend/src/remote/activitypub/kernel/like.ts index 715cc379b..2b65ff738 100644 --- a/packages/backend/src/remote/activitypub/kernel/like.ts +++ b/packages/backend/src/remote/activitypub/kernel/like.ts @@ -1,9 +1,9 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { ILike, getApId } from '../type.js'; import create from '@/services/note/reaction/create.js'; import { fetchNote, extractEmojis } from '../models/note.js'; -export default async (actor: IRemoteUser, activity: ILike) => { +export default async (actor: CacheableRemoteUser, activity: ILike) => { const targetUri = getApId(activity.object); const note = await fetchNote(targetUri); diff --git a/packages/backend/src/remote/activitypub/kernel/read.ts b/packages/backend/src/remote/activitypub/kernel/read.ts index 93cc36ec4..333466e22 100644 --- a/packages/backend/src/remote/activitypub/kernel/read.ts +++ b/packages/backend/src/remote/activitypub/kernel/read.ts @@ -1,10 +1,10 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { IRead, getApId } from '../type.js'; import { isSelfHost, extractDbHost } from '@/misc/convert-host.js'; import { MessagingMessages } from '@/models/index.js'; import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message.js'; -export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise => { +export const performReadActivity = async (actor: CacheableRemoteUser, activity: IRead): Promise => { const id = await getApId(activity.object); if (!isSelfHost(extractDbHost(id))) { diff --git a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts index 72751e83c..824ac69d7 100644 --- a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts @@ -1,11 +1,11 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { remoteReject } from '@/services/following/reject.js'; import { IFollow } from '../../type.js'; import DbResolver from '../../db-resolver.js'; import { relayRejected } from '@/services/relay.js'; import { Users } from '@/models/index.js'; -export default async (actor: IRemoteUser, activity: IFollow): Promise => { +export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある const dbResolver = new DbResolver(); diff --git a/packages/backend/src/remote/activitypub/kernel/reject/index.ts b/packages/backend/src/remote/activitypub/kernel/reject/index.ts index ed86a4aa2..00f08842f 100644 --- a/packages/backend/src/remote/activitypub/kernel/reject/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/reject/index.ts @@ -1,12 +1,12 @@ import Resolver from '../../resolver.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import rejectFollow from './follow.js'; import { IReject, isFollow, getApType } from '../../type.js'; import { apLogger } from '../../logger.js'; const logger = apLogger; -export default async (actor: IRemoteUser, activity: IReject): Promise => { +export default async (actor: CacheableRemoteUser, activity: IReject): Promise => { const uri = activity.id || activity; logger.info(`Reject: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/remove/index.ts b/packages/backend/src/remote/activitypub/kernel/remove/index.ts index 7d7b3386c..11a994a83 100644 --- a/packages/backend/src/remote/activitypub/kernel/remove/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/remove/index.ts @@ -1,9 +1,9 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { IRemove } from '../../type.js'; import { resolveNote } from '../../models/note.js'; import { removePinned } from '@/services/i/pin.js'; -export default async (actor: IRemoteUser, activity: IRemove): Promise => { +export default async (actor: CacheableRemoteUser, activity: IRemove): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts index 2383eea5b..e2f77ca79 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts @@ -1,11 +1,11 @@ import unfollow from '@/services/following/delete.js'; import cancelRequest from '@/services/following/requests/cancel.js'; import {IAccept} from '../../type.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { Followings } from '@/models/index.js'; import DbResolver from '../../db-resolver.js'; -export default async (actor: IRemoteUser, activity: IAccept): Promise => { +export default async (actor: CacheableRemoteUser, activity: IAccept): Promise => { const dbResolver = new DbResolver(); const follower = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts index 822c1e494..f31aca1d8 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts @@ -1,9 +1,9 @@ import { Notes } from '@/models/index.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { IAnnounce, getApId } from '../../type.js'; import deleteNote from '@/services/note/delete.js'; -export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise => { +export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnounce): Promise => { const uri = getApId(activity); const note = await Notes.findOne({ diff --git a/packages/backend/src/remote/activitypub/kernel/undo/block.ts b/packages/backend/src/remote/activitypub/kernel/undo/block.ts index 844b067e2..a4bb5e604 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/block.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/block.ts @@ -1,9 +1,10 @@ import { IBlock } from '../../type.js'; import unblock from '@/services/blocking/delete.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import DbResolver from '../../db-resolver.js'; +import { Users } from '@/models/index.js'; -export default async (actor: IRemoteUser, activity: IBlock): Promise => { +export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { const dbResolver = new DbResolver(); const blockee = await dbResolver.getUserFromApId(activity.object); @@ -15,6 +16,6 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise => return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`; } - await unblock(actor, blockee); + await unblock(await Users.findOneOrFail(actor.id), blockee); return `ok`; }; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts index 6715adcf7..f501cc8cc 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts @@ -1,11 +1,11 @@ import unfollow from '@/services/following/delete.js'; import cancelRequest from '@/services/following/requests/cancel.js'; import { IFollow } from '../../type.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { FollowRequests, Followings } from '@/models/index.js'; import DbResolver from '../../db-resolver.js'; -export default async (actor: IRemoteUser, activity: IFollow): Promise => { +export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { const dbResolver = new DbResolver(); const followee = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/undo/index.ts b/packages/backend/src/remote/activitypub/kernel/undo/index.ts index 05937c685..27d433eb3 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/index.ts @@ -1,5 +1,5 @@ -import { IRemoteUser } from '@/models/entities/user.js'; -import {IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept} from '../../type.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept } from '../../type.js'; import unfollow from './follow.js'; import unblock from './block.js'; import undoLike from './like.js'; @@ -10,7 +10,7 @@ import { apLogger } from '../../logger.js'; const logger = apLogger; -export default async (actor: IRemoteUser, activity: IUndo): Promise => { +export default async (actor: CacheableRemoteUser, activity: IUndo): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/undo/like.ts b/packages/backend/src/remote/activitypub/kernel/undo/like.ts index 08ac63035..01aeba1fb 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/like.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/like.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { ILike, getApId } from '../../type.js'; import deleteReaction from '@/services/note/reaction/delete.js'; import { fetchNote } from '../../models/note.js'; @@ -6,7 +6,7 @@ import { fetchNote } from '../../models/note.js'; /** * Process Undo.Like activity */ -export default async (actor: IRemoteUser, activity: ILike) => { +export default async (actor: CacheableRemoteUser, activity: ILike) => { const targetUri = getApId(activity.object); const note = await fetchNote(targetUri); diff --git a/packages/backend/src/remote/activitypub/kernel/update/index.ts b/packages/backend/src/remote/activitypub/kernel/update/index.ts index 7888c698e..9e8a81bb3 100644 --- a/packages/backend/src/remote/activitypub/kernel/update/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/update/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { getApType, IUpdate, isActor } from '../../type.js'; import { apLogger } from '../../logger.js'; import { updateQuestion } from '../../models/question.js'; @@ -8,7 +8,7 @@ import { updatePerson } from '../../models/person.js'; /** * Updateアクティビティを捌きます */ -export default async (actor: IRemoteUser, activity: IUpdate): Promise => { +export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { return `skip: invalid actor`; } diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts index b5e9181d3..33316dcd9 100644 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ b/packages/backend/src/remote/activitypub/models/image.ts @@ -1,10 +1,10 @@ import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js'; import Resolver from '../resolver.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { apLogger } from '../logger.js'; import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles } from '@/models/index.js'; +import { DriveFiles, Users } from '@/models/index.js'; import { truncate } from '@/misc/truncate.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; @@ -13,7 +13,7 @@ const logger = apLogger; /** * Imageを作成します。 */ -export async function createImage(actor: IRemoteUser, value: any): Promise { +export async function createImage(actor: CacheableRemoteUser, value: any): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); @@ -60,7 +60,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise { +export async function resolveImage(actor: CacheableRemoteUser, value: any): Promise { // TODO // リモートサーバーからフェッチしてきて登録 diff --git a/packages/backend/src/remote/activitypub/models/mention.ts b/packages/backend/src/remote/activitypub/models/mention.ts index c5b0ea53c..a16009296 100644 --- a/packages/backend/src/remote/activitypub/models/mention.ts +++ b/packages/backend/src/remote/activitypub/models/mention.ts @@ -3,17 +3,17 @@ import { IObject, isMention, IApMention } from '../type.js'; import { resolvePerson } from './person.js'; import promiseLimit from 'promise-limit'; import Resolver from '../resolver.js'; -import { User } from '@/models/entities/user.js'; +import { CacheableUser, User } from '@/models/entities/user.js'; export async function extractApMentions(tags: IObject | IObject[] | null | undefined) { const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); const resolver = new Resolver(); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))) - )).filter((x): x is User => x != null); + )).filter((x): x is CacheableUser => x != null); return mentionedUsers; } diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 0bcc89fbb..bdcec6b25 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -5,7 +5,7 @@ import Resolver from '../resolver.js'; import post from '@/services/note/create.js'; import { resolvePerson, updatePerson } from './person.js'; import { resolveImage } from './image.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js'; import { htmlToMfm } from '../misc/html-to-mfm.js'; import { extractApHashtags } from './tag.js'; import { unique, toArray, toSingle } from '@/prelude/array.js'; @@ -15,7 +15,7 @@ import { apLogger } from '../logger.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; import { extractDbHost, toPuny } from '@/misc/convert-host.js'; -import { Emojis, Polls, MessagingMessages } from '@/models/index.js'; +import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js'; import { Emoji } from '@/models/entities/emoji.js'; @@ -90,7 +90,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s logger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ - const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser; + const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser; // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { @@ -230,11 +230,6 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined); - // ユーザーの情報が古かったらついでに更新しておく - if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { - if (actor.uri) updatePerson(actor.uri); - } - if (isTalk) { for (const recipient of visibleUsers) { await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id); diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 659d3ac9a..de64a4305 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -15,7 +15,7 @@ import { apLogger } from '../logger.js'; import { Note } from '@/models/entities/note.js'; import { updateUsertags } from '@/services/update-hashtag.js'; import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js'; -import { User, IRemoteUser } from '@/models/entities/user.js'; +import { User, IRemoteUser, CacheableUser } from '@/models/entities/user.js'; import { Emoji } from '@/models/entities/emoji.js'; import { UserNotePining } from '@/models/entities/user-note-pining.js'; import { genId } from '@/misc/gen-id.js'; @@ -30,6 +30,8 @@ import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { truncate } from '@/misc/truncate.js'; import { StatusError } from '@/misc/fetch.js'; +import { uriPersonCache } from '@/services/user-cache.js'; +import { publishInternalEvent } from '@/services/stream.js'; const logger = apLogger; @@ -91,19 +93,25 @@ function validateActor(x: IObject, uri: string): IActor { * * Misskeyに対象のPersonが登録されていればそれを返します。 */ -export async function fetchPerson(uri: string, resolver?: Resolver): Promise { +export async function fetchPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); + const cached = uriPersonCache.get(uri); + if (cached) return cached; + // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(config.url + '/')) { const id = uri.split('/').pop(); - return await Users.findOne(id).then(x => x || null); + const u = await Users.findOne(id).then(x => x || null); // TODO: typeorm 3.0 にしたら .then(x => x || null) を消す + if (u) uriPersonCache.set(uri, u); + return u; } //#region このサーバーに既に登録されていたらそれを返す const exist = await Users.findOne({ uri }); if (exist) { + uriPersonCache.set(uri, exist); return exist; } //#endregion @@ -352,6 +360,8 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint location: person['vcard:Address'] || null, }); + publishInternalEvent('remoteUserUpdated', { id: exist.id }); + // ハッシュタグ更新 updateUsertags(exist, tags); @@ -371,7 +381,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolvePerson(uri: string, resolver?: Resolver): Promise { +export async function resolvePerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); //#region このサーバーに既に登録されていたらそれを返す diff --git a/packages/backend/src/remote/activitypub/perform.ts b/packages/backend/src/remote/activitypub/perform.ts index 3e1881558..a3c10ba94 100644 --- a/packages/backend/src/remote/activitypub/perform.ts +++ b/packages/backend/src/remote/activitypub/perform.ts @@ -1,7 +1,17 @@ import { IObject } from './type.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { performActivity } from './kernel/index.js'; +import { updatePerson } from './models/person.js'; -export default async (actor: IRemoteUser, activity: IObject): Promise => { +export default async (actor: CacheableRemoteUser, activity: IObject): Promise => { await performActivity(actor, activity); + + // ついでにリモートユーザーの情報が古かったら更新しておく + if (actor.uri) { + if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { + setImmediate(() => { + updatePerson(actor.uri!); + }); + } + } }; diff --git a/packages/backend/src/server/activitypub/cache.ts b/packages/backend/src/server/activitypub/cache.ts deleted file mode 100644 index eb20d0078..000000000 --- a/packages/backend/src/server/activitypub/cache.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Cache } from "@/misc/cache.js"; -import { User } from "@/models/entities/user.js"; - -export const userCache = new Cache(1000 * 60 * 30); diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts index fdae9dd92..f2bdb48bf 100644 --- a/packages/backend/src/server/activitypub/followers.ts +++ b/packages/backend/src/server/activitypub/followers.ts @@ -10,7 +10,6 @@ import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; import { setResponseType } from '../activitypub.js'; import { Users, Followings, UserProfiles } from '@/models/index.js'; import { LessThan } from 'typeorm'; -import { userCache } from './cache.js'; export default async (ctx: Router.RouterContext) => { const userId = ctx.params.user; @@ -28,11 +27,10 @@ export default async (ctx: Router.RouterContext) => { return; } - // TODO: typeorm 3.0にしたら .then(x => x || null) は消せる - const user = await userCache.fetch(userId, () => Users.findOne({ + const user = await Users.findOne({ id: userId, host: null, - }).then(x => x || null)); + }); if (user == null) { ctx.status = 404; diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts index eb1b7a9d8..16b2b051d 100644 --- a/packages/backend/src/server/activitypub/following.ts +++ b/packages/backend/src/server/activitypub/following.ts @@ -11,7 +11,6 @@ import { setResponseType } from '../activitypub.js'; import { Users, Followings, UserProfiles } from '@/models/index.js'; import { LessThan, FindConditions } from 'typeorm'; import { Following } from '@/models/entities/following.js'; -import { userCache } from './cache.js'; export default async (ctx: Router.RouterContext) => { const userId = ctx.params.user; @@ -29,11 +28,10 @@ export default async (ctx: Router.RouterContext) => { return; } - // TODO: typeorm 3.0にしたら .then(x => x || null) は消せる - const user = await userCache.fetch(userId, () => Users.findOne({ + const user = await Users.findOne({ id: userId, host: null, - }).then(x => x || null)); + }); if (user == null) { ctx.status = 404; diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts index db2a18efc..450fdea96 100644 --- a/packages/backend/src/server/activitypub/outbox.ts +++ b/packages/backend/src/server/activitypub/outbox.ts @@ -15,7 +15,6 @@ import { Users, Notes } from '@/models/index.js'; import { makePaginationQuery } from '../api/common/make-pagination-query.js'; import { Brackets } from 'typeorm'; import { Note } from '@/models/entities/note.js'; -import { userCache } from './cache.js'; export default async (ctx: Router.RouterContext) => { const userId = ctx.params.user; @@ -36,11 +35,10 @@ export default async (ctx: Router.RouterContext) => { return; } - // TODO: typeorm 3.0にしたら .then(x => x || null) は消せる - const user = await userCache.fetch(userId, () => Users.findOne({ + const user = await Users.findOne({ id: userId, host: null, - }).then(x => x || null)); + }); if (user == null) { ctx.status = 404; diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts index 7fdf14666..8fb397ca5 100644 --- a/packages/backend/src/server/api/authenticate.ts +++ b/packages/backend/src/server/api/authenticate.ts @@ -1,7 +1,12 @@ import isNativeToken from './common/is-native-token.js'; -import { User } from '@/models/entities/user.js'; +import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js'; import { Users, AccessTokens, Apps } from '@/models/index.js'; import { AccessToken } from '@/models/entities/access-token.js'; +import { Cache } from '@/misc/cache.js'; +import { App } from '@/models/entities/app.js'; +import { localUserByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js'; + +const appCache = new Cache(Infinity); export class AuthenticationError extends Error { constructor(message: string) { @@ -10,15 +15,15 @@ export class AuthenticationError extends Error { } } -export default async (token: string | null): Promise<[User | null | undefined, AccessToken | null | undefined]> => { +export default async (token: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => { if (token == null) { return [null, null]; } if (isNativeToken(token)) { - // Fetch user - const user = await Users - .findOne({ token }); + // TODO: typeorm 3.0にしたら .then(x => x || null) は消せる + const user = await localUserByNativeTokenCache.fetch(token, + () => Users.findOne({ token }).then(x => x || null) as Promise); if (user == null) { throw new AuthenticationError('user not found'); @@ -42,14 +47,14 @@ export default async (token: string | null): Promise<[User | null | undefined, A lastUsedAt: new Date(), }); - const user = await Users - .findOne({ + const user = await localUserByIdCache.fetch(accessToken.userId, + () => Users.findOne({ id: accessToken.userId, // findOne(accessToken.userId) のように書かないのは後方互換性のため - }); + }) as Promise); if (accessToken.appId) { - const app = await Apps - .findOneOrFail(accessToken.appId); + const app = await appCache.fetch(accessToken.appId, + () => Apps.findOneOrFail(accessToken.appId!)); return [user, { id: accessToken.id, diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index 5c5ef6601..9a85e4565 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -1,7 +1,7 @@ import Koa from 'koa'; import { performance } from 'perf_hooks'; import { limiter } from './limiter.js'; -import { User } from '@/models/entities/user.js'; +import { CacheableLocalUser, User } from '@/models/entities/user.js'; import endpoints, { IEndpoint } from './endpoints.js'; import { ApiError } from './error.js'; import { apiLogger } from './logger.js'; @@ -13,7 +13,7 @@ const accessDenied = { id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', }; -export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { +export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { const isSecure = user != null && token == null; const ep = endpoints.find(e => e.name === endpoint); diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts index 4e6d041a2..152989434 100644 --- a/packages/backend/src/server/api/define.ts +++ b/packages/backend/src/server/api/define.ts @@ -1,30 +1,16 @@ import * as fs from 'node:fs'; import Ajv from 'ajv'; -import { ILocalUser } from '@/models/entities/user.js'; +import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js'; import { IEndpointMeta } from './endpoints.js'; import { ApiError } from './error.js'; import { Schema, SchemaType } from '@/misc/schema.js'; import { AccessToken } from '@/models/entities/access-token.js'; -type SimpleUserInfo = { - id: ILocalUser['id']; - createdAt: ILocalUser['createdAt']; - host: ILocalUser['host']; - username: ILocalUser['username']; - uri: ILocalUser['uri']; - inbox: ILocalUser['inbox']; - sharedInbox: ILocalUser['sharedInbox']; - isAdmin: ILocalUser['isAdmin']; - isModerator: ILocalUser['isModerator']; - isSilenced: ILocalUser['isSilenced']; - showTimelineReplies: ILocalUser['showTimelineReplies']; -}; - export type Response = Record | void; // TODO: paramsの型をT['params']のスキーマ定義から推論する type executor = - (params: SchemaType, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any, cleanup?: () => any) => + (params: SchemaType, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) => Promise>>; const ajv = new Ajv({ @@ -34,11 +20,11 @@ const ajv = new Ajv({ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); export default function (meta: T, paramDef: Ps, cb: executor) - : (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => Promise { + : (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise { const validate = ajv.compile(paramDef); - return (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => { + return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => { function cleanup() { fs.unlink(file.path, () => {}); } diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts index 4206e3a3c..60bf0ff09 100644 --- a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts @@ -1,5 +1,6 @@ import define from '../../../define.js'; import { Users } from '@/models/index.js'; +import { publishInternalEvent } from '@/services/stream.js'; export const meta = { tags: ['admin'], @@ -31,4 +32,6 @@ export default define(meta, paramDef, async (ps) => { await Users.update(user.id, { isModerator: true, }); + + publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: true }); }); diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts index 143119bfe..cf9d7c14a 100644 --- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts @@ -27,4 +27,6 @@ export default define(meta, paramDef, async (ps) => { await Users.update(user.id, { isModerator: false, }); + + publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: false }); }); diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index a435dcc28..564b8a026 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -29,7 +29,8 @@ export default define(meta, paramDef, async (ps, me) => { throw new Error('user not found'); } - if ((me.isModerator && !me.isAdmin) && user.isAdmin) { + const _me = await Users.findOneOrFail(me.id); + if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) { throw new Error('cannot show info of admin'); } diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts index 4a74c3fb0..4cbed1cf9 100644 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts @@ -1,6 +1,7 @@ import define from '../../define.js'; import { Users } from '@/models/index.js'; import { insertModerationLog } from '@/services/insert-moderation-log.js'; +import { publishInternalEvent } from '@/services/stream.js'; export const meta = { tags: ['admin'], @@ -33,6 +34,8 @@ export default define(meta, paramDef, async (ps, me) => { isSilenced: true, }); + publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: true }); + insertModerationLog(me, 'silence', { targetId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts index 4e6366aa1..6c6628811 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts @@ -1,6 +1,7 @@ import define from '../../define.js'; import { Users } from '@/models/index.js'; import { insertModerationLog } from '@/services/insert-moderation-log.js'; +import { publishInternalEvent } from '@/services/stream.js'; export const meta = { tags: ['admin'], @@ -29,6 +30,8 @@ export default define(meta, paramDef, async (ps, me) => { isSilenced: false, }); + publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: false }); + insertModerationLog(me, 'unsilence', { targetId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index 5f565a63f..eac8eabfc 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -2,7 +2,7 @@ import { deleteFile } from '@/services/drive/delete-file.js'; import { publishDriveStream } from '@/services/stream.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { DriveFiles } from '@/models/index.js'; +import { DriveFiles, Users } from '@/models/index.js'; export const meta = { tags: ['drive'], @@ -42,7 +42,7 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchFile); } - if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { + if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index 181365c7e..16b313cab 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -1,7 +1,7 @@ import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles } from '@/models/index.js'; +import { DriveFiles, Users } from '@/models/index.js'; export const meta = { tags: ['drive'], @@ -70,7 +70,7 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchFile); } - if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { + if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index ab8e4aeeb..30d7847b5 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -1,7 +1,7 @@ import { publishDriveStream } from '@/services/stream.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { DriveFiles, DriveFolders } from '@/models/index.js'; +import { DriveFiles, DriveFolders, Users } from '@/models/index.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; export const meta = { @@ -64,7 +64,7 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchFile); } - if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { + if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } 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 771c98b21..ae23d2482 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -1,5 +1,5 @@ import bcrypt from 'bcryptjs'; -import { publishMainStream, publishUserEvent } from '@/services/stream.js'; +import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js'; import generateUserToken from '../../common/generate-native-user-token.js'; import define from '../../define.js'; import { Users, UserProfiles } from '@/models/index.js'; @@ -20,6 +20,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { + const freshUser = await Users.findOneOrFail(user.id); + const oldToken = freshUser.token; + const profile = await UserProfiles.findOneOrFail(user.id); // Compare password @@ -29,14 +32,14 @@ export default define(meta, paramDef, async (ps, user) => { throw new Error('incorrect password'); } - // Generate secret - const secret = generateUserToken(); + const newToken = generateUserToken(); await Users.update(user.id, { - token: secret, + token: newToken, }); // Publish event + publishInternalEvent('userTokenRegenerated', { id: user.id, oldToken, newToken }); publishMainStream(user.id, 'myTokenRegenerated'); // Terminate streaming diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 22ff2275c..a1ab06d46 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -48,7 +48,7 @@ export default define(meta, paramDef, async (ps, user) => { throw e; }); - if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) { + if ((!user.isAdmin && !user.isModerator) && (note.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 26aaa0919..09a819466 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -2,7 +2,7 @@ import define from '../../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { ApiError } from '../../error.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Notes } from '@/models/index.js'; +import { Notes, Users } from '@/models/index.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; import { activeUsersChart } from '@/services/chart/index.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 9bcb64b65..7c9c12296 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -2,7 +2,7 @@ import define from '../../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { ApiError } from '../../error.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Followings, Notes } from '@/models/index.js'; +import { Followings, Notes, Users } from '@/models/index.js'; import { Brackets } from 'typeorm'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; @@ -56,7 +56,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const m = await fetchMeta(); - if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) { + if (m.disableLocalTimeline && (!user.isAdmin && !user.isModerator)) { throw new ApiError(meta.errors.stlDisabled); } 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 12fc88b1f..bb0bbe2a2 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,7 +1,7 @@ import define from '../../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { ApiError } from '../../error.js'; -import { Notes } from '@/models/index.js'; +import { Notes, Users } from '@/models/index.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 70db12fb1..3555424fa 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -1,5 +1,4 @@ import define from '../../define.js'; -import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; import { NoteReactions } from '@/models/index.js'; import { DeepPartial } from 'typeorm'; diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts index 7e6b93b39..e74db8466 100644 --- a/packages/backend/src/server/api/limiter.ts +++ b/packages/backend/src/server/api/limiter.ts @@ -2,12 +2,12 @@ import Limiter from 'ratelimiter'; import { redisClient } from '../../db/redis.js'; import { IEndpoint } from './endpoints.js'; import * as Acct from '@/misc/acct.js'; -import { User } from '@/models/entities/user.js'; +import { CacheableLocalUser, User } from '@/models/entities/user.js'; import Logger from '@/services/logger.js'; const logger = new Logger('limiter'); -export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable } }, user: User) => new Promise((ok, reject) => { +export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable } }, user: CacheableLocalUser) => new Promise((ok, reject) => { const limitation = endpoint.meta.limit; const key = Object.prototype.hasOwnProperty.call(limitation, 'key') diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index 90cf59038..bea863eb7 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -18,6 +18,11 @@ import { Packed } from '@/misc/schema.js'; //#region Stream type-body definitions export interface InternalStreamTypes { + userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; }; + userChangeSilencedState: { id: User['id']; isSilenced: User['isSilenced']; }; + userChangeModeratorState: { id: User['id']; isModerator: User['isModerator']; }; + userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; }; + remoteUserUpdated: { id: User['id']; }; antennaCreated: Antenna; antennaDeleted: Antenna; antennaUpdated: Antenna; diff --git a/packages/backend/src/services/following/reject.ts b/packages/backend/src/services/following/reject.ts index 3b0cb2ba8..a108d1c1c 100644 --- a/packages/backend/src/services/following/reject.ts +++ b/packages/backend/src/services/following/reject.ts @@ -7,8 +7,17 @@ import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; import { Users, FollowRequests, Followings } from '@/models/index.js'; import { decrementFollowing } from './delete.js'; -type Local = ILocalUser | { id: User['id']; host: User['host']; uri: User['host'] }; -type Remote = IRemoteUser; +type Local = ILocalUser | { + id: ILocalUser['id']; + host: ILocalUser['host']; + uri: ILocalUser['uri'] +}; +type Remote = IRemoteUser | { + id: IRemoteUser['id']; + host: IRemoteUser['host']; + uri: IRemoteUser['uri']; + inbox: IRemoteUser['inbox']; +}; type Both = Local | Remote; /** diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts index c3908b255..17479a85f 100644 --- a/packages/backend/src/services/messages/create.ts +++ b/packages/backend/src/services/messages/create.ts @@ -1,4 +1,4 @@ -import { User } from '@/models/entities/user.js'; +import { CacheableUser, User } from '@/models/entities/user.js'; import { UserGroup } from '@/models/entities/user-group.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/index.js'; @@ -13,7 +13,7 @@ import renderCreate from '@/remote/activitypub/renderer/create.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { deliver } from '@/queue/index.js'; -export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) { +export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: CacheableUser | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) { const message = { id: genId(), createdAt: new Date(), diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index b295534cd..f4b0d5204 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -38,8 +38,6 @@ import { endedPollNotificationQueue } from '@/queue/queues.js'; import { Cache } from '@/misc/cache.js'; import { UserProfile } from '@/models/entities/user-profile.js'; -const usersCache = new Cache(Infinity); - const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -212,7 +210,7 @@ export default async (user: { id: User['id']; username: User['username']; host: tags = tags.filter(tag => Array.from(tag || '').length <= 128).splice(0, 32); if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { - mentionedUsers.push(await usersCache.fetch(data.reply.userId, () => Users.findOneOrFail(data.reply!.userId))); + mentionedUsers.push(await Users.findOneOrFail(data.reply!.userId)); } if (data.visibility === 'specified') { @@ -225,7 +223,7 @@ export default async (user: { id: User['id']; username: User['username']; host: } if (data.reply && !data.visibleUsers.some(x => x.id === data.reply!.userId)) { - data.visibleUsers.push(await usersCache.fetch(data.reply.userId, () => Users.findOneOrFail(data.reply!.userId))); + data.visibleUsers.push(await Users.findOneOrFail(data.reply!.userId)); } } diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index 356dc3972..1caac2b88 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -20,7 +20,7 @@ import { Brackets, In } from 'typeorm'; * @param user 投稿者 * @param note 投稿 */ -export default async function(user: User, note: Note, quiet = false) { +export default async function(user: { id: User['id']; uri: User['uri']; host: User['host']; }, note: Note, quiet = false) { const deletedAt = new Date(); // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき @@ -131,7 +131,7 @@ async function getMentionedRemoteUsers(note: Note) { }) as IRemoteUser[]; } -async function deliverToConcerned(user: ILocalUser, note: Note, content: any) { +async function deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any) { deliverToFollowers(user, content); deliverToRelays(user, content); const remoteUsers = await getMentionedRemoteUsers(note); diff --git a/packages/backend/src/services/note/polls/vote.ts b/packages/backend/src/services/note/polls/vote.ts index 9b83b1953..c758e3857 100644 --- a/packages/backend/src/services/note/polls/vote.ts +++ b/packages/backend/src/services/note/polls/vote.ts @@ -1,12 +1,12 @@ import { publishNoteStream } from '@/services/stream.js'; -import { User } from '@/models/entities/user.js'; +import { CacheableUser, User } from '@/models/entities/user.js'; import { Note } from '@/models/entities/note.js'; import { PollVotes, NoteWatchings, Polls, Blockings } from '@/models/index.js'; import { Not } from 'typeorm'; import { genId } from '@/misc/gen-id.js'; import { createNotification } from '../../create-notification.js'; -export default async function(user: User, note: Note, choice: number) { +export default async function(user: CacheableUser, note: Note, choice: number) { const poll = await Polls.findOne(note.id); if (poll == null) throw new Error('poll not found'); diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts index 6f0da503f..ef04e1edf 100644 --- a/packages/backend/src/services/relay.ts +++ b/packages/backend/src/services/relay.ts @@ -6,9 +6,13 @@ import { deliver } from '@/queue/index.js'; import { ILocalUser, User } from '@/models/entities/user.js'; import { Users, Relays } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; +import { Cache } from '@/misc/cache.js'; +import { Relay } from '@/models/entities/relay.js'; const ACTOR_USERNAME = 'relay.actor' as const; +const relaysCache = new Cache(1000 * 60 * 10); + export async function getRelayActor(): Promise { const user = await Users.findOne({ host: null, @@ -78,9 +82,9 @@ export async function relayRejected(id: string) { export async function deliverToRelays(user: { id: User['id']; host: null; }, activity: any) { if (activity == null) return; - const relays = await Relays.find({ + const relays = await relaysCache.fetch(null, () => Relays.find({ status: 'accepted', - }); + })); if (relays.length === 0) return; const copy = JSON.parse(JSON.stringify(activity)); diff --git a/packages/backend/src/services/suspend-user.ts b/packages/backend/src/services/suspend-user.ts index 033311a3c..57999763c 100644 --- a/packages/backend/src/services/suspend-user.ts +++ b/packages/backend/src/services/suspend-user.ts @@ -5,8 +5,11 @@ import config from '@/config/index.js'; import { User } from '@/models/entities/user.js'; import { Users, Followings } from '@/models/index.js'; import { Not, IsNull } from 'typeorm'; +import { publishInternalEvent } from './stream'; export async function doPostSuspend(user: { id: User['id']; host: User['host'] }) { + publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); + if (Users.isLocalUser(user)) { // 知り得る全SharedInboxにDelete配信 const content = renderActivity(renderDelete(`${config.url}/users/${user.id}`, user)); diff --git a/packages/backend/src/services/unsuspend-user.ts b/packages/backend/src/services/unsuspend-user.ts index 3be081d0e..b1f0284ac 100644 --- a/packages/backend/src/services/unsuspend-user.ts +++ b/packages/backend/src/services/unsuspend-user.ts @@ -6,8 +6,11 @@ import config from '@/config/index.js'; import { User } from '@/models/entities/user.js'; import { Users, Followings } from '@/models/index.js'; import { Not, IsNull } from 'typeorm'; +import { publishInternalEvent } from './stream'; export async function doPostUnsuspend(user: User) { + publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); + if (Users.isLocalUser(user)) { // 知り得る全SharedInboxにUndo Delete配信 const content = renderActivity(renderUndo(renderDelete(`${config.url}/users/${user.id}`, user), user)); diff --git a/packages/backend/src/services/user-cache.ts b/packages/backend/src/services/user-cache.ts new file mode 100644 index 000000000..4cf3526b7 --- /dev/null +++ b/packages/backend/src/services/user-cache.ts @@ -0,0 +1,44 @@ +import { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/user.js'; +import { Users } from '@/models/index.js'; +import { Cache } from '@/misc/cache.js'; +import { subsdcriber } from '@/db/redis.js'; + +export const userByIdCache = new Cache(Infinity); +export const localUserByNativeTokenCache = new Cache(Infinity); +export const localUserByIdCache = new Cache(Infinity); +export const uriPersonCache = new Cache(Infinity); + +subsdcriber.on('message', async (_, data) => { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message; + switch (type) { + case 'userChangeSuspendedState': + case 'userChangeSilencedState': + case 'userChangeModeratorState': + case 'remoteUserUpdated': { + const user = await Users.findOneOrFail(body.id); + userByIdCache.set(user.id, user); + for (const [k, v] of uriPersonCache.cache.entries()) { + if (v.value?.id === user.id) { + uriPersonCache.set(k, user); + } + } + if (Users.isLocalUser(user)) { + localUserByNativeTokenCache.set(user.token, user); + localUserByIdCache.set(user.id, user); + } + break; + } + case 'userTokenRegenerated': { + const user = await Users.findOneOrFail(body.id) as ILocalUser; + localUserByNativeTokenCache.delete(body.oldToken); + localUserByNativeTokenCache.set(body.newToken, user); + break; + } + default: + break; + } + } +}); diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue index 8ed29d5c2..e991d725b 100644 --- a/packages/client/src/pages/settings/profile.vue +++ b/packages/client/src/pages/settings/profile.vue @@ -54,7 +54,7 @@ {{ i18n.ts.flagAsCat }} - {{ i18n.ts.flagShowTimelineReplies }} + {{ i18n.ts.flagShowTimelineReplies }} {{ i18n.ts.flagAsBot }} {{ i18n.ts.alwaysMarkSensitive }} diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue index 4bdc82f60..516ab4d44 100644 --- a/packages/client/src/pages/user-info.vue +++ b/packages/client/src/pages/user-info.vue @@ -25,6 +25,7 @@ {{ $ts.moderator }} {{ $ts.silence }} {{ $ts.suspend }} + {{ $ts.reflectMayTakeTime }} {{ $ts.resetPassword }}