From fb65fcf890fb22bdada5feb55f22368a95820cbb Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 15 Sep 2025 14:51:08 -0400 Subject: [PATCH 01/16] use CacheService in users/show endpoint --- .../src/server/api/endpoints/users/show.ts | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 30b4719e09..e617e98995 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -14,6 +14,7 @@ import { DI } from '@/di-symbols.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; import { RoleService } from '@/core/RoleService.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../error.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import type { FindOptionsWhere } from 'typeorm'; @@ -103,6 +104,7 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, private perUserPvChart: PerUserPvChart, private apiLoggerService: ApiLoggerService, + private readonly cacheService: CacheService, ) { super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => { let user; @@ -115,19 +117,18 @@ export default class extends Endpoint { // eslint- return []; } - const users = await this.usersRepository.findBy(isModerator ? { - id: In(ps.userIds), - } : { - id: In(ps.userIds), - isSuspended: false, - }); + const users = await this.cacheService.getUsers(ps.userIds); // リクエストされた通りに並べ替え // 順番は保持されるけど数は減ってる可能性がある const _users: MiUser[] = []; for (const id of ps.userIds) { - const user = users.find(x => x.id === id); - if (user != null) _users.push(user); + const user = users.get(id); + if (user != null) { + if (isModerator || !user.isSuspended) { + _users.push(user); + } + } } const _userMap = await this.userEntityService.packMany(_users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' }) @@ -140,12 +141,10 @@ export default class extends Endpoint { // eslint- this.apiLoggerService.logger.warn(`failed to resolve remote user: ${renderInlineError(err)}`); throw new ApiError(meta.errors.failedToResolveRemoteUser); }); - } else { - const q: FindOptionsWhere = ps.userId != null - ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: IsNull() }; - - user = await this.usersRepository.findOneBy(q); + } else if (ps.userId != null) { + user = await this.cacheService.findUserById(ps.userId); + } else if (ps.username) { + user = await this.usersRepository.findOneBy({ usernameLower: ps.username.toLowerCase(), host: IsNull() }); } if (user == null || (!isModerator && user.isSuspended)) { From f37614b751ce0036272365152bd6e74add2e4935 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 15 Sep 2025 17:56:26 -0400 Subject: [PATCH 02/16] move parseUri from ApDbResolverService to UtilityService --- packages/backend/src/core/UtilityService.ts | 36 +++++++++++++++++++ .../core/activitypub/ApDbResolverService.ts | 35 +++--------------- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 8ebf7e6f52..63b16a55d8 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -16,6 +16,22 @@ import type { MiInstance } from '@/models/Instance.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { EnvService } from '@/global/EnvService.js'; +export type UriParseResult = { + /** wether the URI was generated by us */ + local: true; + /** id in DB */ + id: string; + /** hint of type, e.g. "notes", "users" */ + type: string; + /** any remaining text after type and id, not including the slash after id. undefined if empty */ + rest?: string; +} | { + /** wether the URI was generated by us */ + local: false; + /** uri in DB */ + uri: string; +}; + @Injectable() export class UtilityService { constructor( @@ -302,4 +318,24 @@ export class UtilityService { return false; } } + + // Moved from ApPersonService to avoid circular dependency + @bindThis + public parseUri(value: string | IObject | [string | IObject]): UriParseResult { + const separator = '/'; + + const apId = getApId(value); + const uri = new URL(apId); + if (this.toPuny(uri.host) !== this.toPuny(this.config.host)) { + return { local: false, uri: apId }; + } + + const [, type, id, ...rest] = uri.pathname.split(separator); + return { + local: true, + type, + id, + rest: rest.length === 0 ? undefined : rest.join(separator), + }; + } } diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index a5eb851d6c..f79d6a86a8 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -19,21 +19,7 @@ import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { IObject } from './type.js'; -export type UriParseResult = { - /** wether the URI was generated by us */ - local: true; - /** id in DB */ - id: string; - /** hint of type, e.g. "notes", "users" */ - type: string; - /** any remaining text after type and id, not including the slash after id. undefined if empty */ - rest?: string; -} | { - /** wether the URI was generated by us */ - local: false; - /** uri in DB */ - uri: string; -}; +export type { UriParseResult } from '@/core/UtilityService.js'; @Injectable() export class ApDbResolverService implements OnApplicationShutdown { @@ -58,23 +44,10 @@ export class ApDbResolverService implements OnApplicationShutdown { // Caches moved to ApPersonService to avoid circular dependency } + // Moved to UtilityService to avoid circular dependency @bindThis - public parseUri(value: string | IObject | [string | IObject]): UriParseResult { - const separator = '/'; - - const apId = getApId(value); - const uri = new URL(apId); - if (this.utilityService.toPuny(uri.host) !== this.utilityService.toPuny(this.config.host)) { - return { local: false, uri: apId }; - } - - const [, type, id, ...rest] = uri.pathname.split(separator); - return { - local: true, - type, - id, - rest: rest.length === 0 ? undefined : rest.join(separator), - }; + public parseUri(value: string | IObject | [string | IObject]) { + return this.utilityService.parseUri(value); } /** From 4d09ccbcc81e0ad15c93d99287504ee367a84750 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 15 Sep 2025 17:57:30 -0400 Subject: [PATCH 03/16] simplify logic in users/show lookup --- .../src/server/api/endpoints/users/show.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index e617e98995..3fcfa8490a 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -13,7 +13,6 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DI } from '@/di-symbols.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; import { RoleService } from '@/core/RoleService.js'; -import { renderInlineError } from '@/misc/render-inline-error.js'; import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../error.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; @@ -136,15 +135,14 @@ export default class extends Endpoint { // eslint- return _users.map(u => _userMap.get(u.id)!); } else { // Lookup user - if (typeof ps.host === 'string' && typeof ps.username === 'string') { - user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host).catch(err => { - this.apiLoggerService.logger.warn(`failed to resolve remote user: ${renderInlineError(err)}`); - throw new ApiError(meta.errors.failedToResolveRemoteUser); - }); + if (ps.username) { + user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host ?? null).catch(() => null); } else if (ps.userId != null) { - user = await this.cacheService.findUserById(ps.userId); - } else if (ps.username) { - user = await this.usersRepository.findOneBy({ usernameLower: ps.username.toLowerCase(), host: IsNull() }); + user = await this.cacheService.findUserById(ps.userId).catch(() => null); + } + + if (user == null && ps.host != null) { + throw new ApiError(meta.errors.failedToResolveRemoteUser); } if (user == null || (!isModerator && user.isSuspended)) { From 35eecf02b39dd86ea25889dee147f733c1a9868a Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 15 Sep 2025 18:26:13 -0400 Subject: [PATCH 04/16] clean up and improve logic in ApPersonService.fetchPerson --- .../activitypub/models/ApPersonService.ts | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 25cd0ce8c0..927ce9e574 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -293,28 +293,38 @@ export class ApPersonService implements OnModuleInit { * Misskeyに対象のPersonが登録されていればそれを返し、登録がなければnullを返します。 */ @bindThis - public async fetchPerson(uri: string): Promise { - const cached = await this.uriPersonCache.fetchMaybe(uri); - if (cached) return await this.cacheService.findOptionalUserById(cached) as MiRemoteUser | MiLocalUser | undefined ?? null; + public async fetchPerson(uri: string, opts?: { withDeleted?: boolean, withSuspended?: boolean }): Promise { + const _opts = { + withDeleted: opts?.withDeleted ?? false, + withSuspended: opts?.withSuspended ?? true, + }; - // URIがこのサーバーを指しているならデータベースからフェッチ - if (uri.startsWith(`${this.config.url}/`)) { - const id = uri.split('/').pop(); - const u = await this.usersRepository.findOneBy({ id }) as MiLocalUser | null; - if (u) await this.uriPersonCache.set(uri, u.id); - return u; + let userId; + + // Resolve URI -> User ID + const parsed = this.utilityService.parseUri(uri); + if (parsed.local) { + userId = parsed.type === 'users' ? parsed.id : null; + } else { + userId = await this.cacheService.uriPersonCache.fetch(uri); } - //#region このサーバーに既に登録されていたらそれを返す - const exist = await this.usersRepository.findOneBy({ uri }) as MiLocalUser | MiRemoteUser | null; - - if (exist) { - await this.uriPersonCache.set(uri, exist.id); - return exist; + // No match + if (!userId) { + return null; } - //#endregion - return null; + const user = await this.cacheService.findUserById(userId) + .catch(() => null) as MiLocalUser | MiRemoteUser | null; + + if (user?.isDeleted && !_opts.withDeleted) { + return null; + } + if (user?.isSuspended && !_opts.withSuspended) { + return null; + } + + return user; } private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any, bgimg: any): Promise>> { @@ -851,7 +861,7 @@ export class ApPersonService implements OnModuleInit { } //#region このサーバーに既に登録されていたらそれを返す - const exist = await this.fetchPerson(uri); + const exist = await this.fetchPerson(uri, { withDeleted: true }); if (exist) return exist; //#endregion From fdc1cc23680104b870d5c8d29215f37644cc2298 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 15 Sep 2025 18:27:22 -0400 Subject: [PATCH 05/16] remove duplicate function from ApDbResolverService --- .../core/activitypub/ApDbResolverService.ts | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index f79d6a86a8..87e3b7ed5f 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -75,31 +75,8 @@ export class ApDbResolverService implements OnApplicationShutdown { */ @bindThis public async getUserFromApId(value: string | IObject | [string | IObject]): Promise { - const parsed = this.parseUri(value); - - if (parsed.local) { - if (parsed.type !== 'users') return null; - - const u = await this.cacheService.findOptionalUserById(parsed.id); - - if (u == null || u.isDeleted) { - return null; - } - - return u as MiLocalUser | MiRemoteUser; - } else { - const uid = await this.apPersonService.uriPersonCache.fetchMaybe(parsed.uri); - if (uid == null) { - return null; - } - - const u = await this.cacheService.findOptionalUserById(uid); - if (u == null || u.isDeleted) { - return null; - } - - return u as MiLocalUser | MiRemoteUser; - } + const uri = getApId(value); + return await this.apPersonService.fetchPerson(uri); } /** From 069d13c77ff78837dda8e2a735d42cf389dfc5b6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 15 Sep 2025 18:27:46 -0400 Subject: [PATCH 06/16] refactor RemoteUserResolveService to avoid exceptions while refreshing remote users --- .../src/core/RemoteUserResolveService.ts | 144 +++++++++++------- 1 file changed, 86 insertions(+), 58 deletions(-) diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index df2c307780..3860ceb15b 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -11,18 +11,23 @@ import type { UsersRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; +import { isRemoteUser } from '@/models/User.js'; import { UtilityService } from '@/core/UtilityService.js'; import { ILink, WebfingerService } from '@/core/WebfingerService.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; -import { TimeService } from '@/global/TimeService.js'; import { bindThis } from '@/decorators.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; +import { CacheService } from '@/core/CacheService.js'; +import * as Acct from '@/misc/acct.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { InternalEventService } from '@/core/InternalEventService.js'; @Injectable() export class RemoteUserResolveService { private logger: Logger; + private readonly selfHost: string; constructor( @Inject(DI.config) @@ -36,88 +41,111 @@ export class RemoteUserResolveService { private remoteLoggerService: RemoteLoggerService, private apDbResolverService: ApDbResolverService, private apPersonService: ApPersonService, - private readonly timeService: TimeService, + private readonly cacheService: CacheService, + private readonly internalEventService: InternalEventService, ) { this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user'); + this.selfHost = this.utilityService.toPuny(this.config.host); } @bindThis public async resolveUser(username: string, host: string | null): Promise { - const usernameLower = username.toLowerCase(); + // Normalize inputs + username = username.toLowerCase(); + host = host ? this.utilityService.toPuny(host) : null; // unicode -> punycode + host = host !== this.selfHost ? host : null; // self-host -> null + const acct = Acct.toString({ username, host }); // username+host -> acct (handle) - if (host == null) { - return await this.usersRepository.findOneByOrFail({ usernameLower, host: IsNull() }) as MiLocalUser; + // Try fetch from DB + let user = await this.cacheService.findUserByAcct(acct).catch(() => null); // Error is expected if the user doesn't exist yet + + // Opportunistically update remote users + if (user != null && isRemoteUser(user)) { + user = await this.tryUpdateUser(user, acct); } - host = this.utilityService.toPuny(host); - - if (host === this.utilityService.toPuny(this.config.host)) { - return await this.usersRepository.findOneByOrFail({ usernameLower, host: IsNull() }) as MiLocalUser; + // Try resolve from AP + if (user == null && host != null) { + user = await this.tryCreateUser(acct); } - const user = await this.usersRepository.findOneBy({ usernameLower, host }) as MiRemoteUser | null; - - const acctLower = `${usernameLower}@${host}`; - + // Failed to fetch or resolve if (user == null) { - const self = await this.resolveSelf(acctLower); + throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', `Could not resolve user ${acct}`); + } + + return user as MiLocalUser | MiRemoteUser; + } + + @bindThis + private async tryCreateUser(acct: string): Promise { + try { + const self = await this.resolveSelf(acct); if (this.utilityService.isUriLocal(self.href)) { - const local = this.apDbResolverService.parseUri(self.href); - if (local.local && local.type === 'users') { - // the LR points to local - return (await this.apDbResolverService - .getUserFromApId(self.href) - .then((u) => { - if (u == null) { - throw new Error(`local user not found: ${self.href}`); - } else { - return u; - } - })) as MiLocalUser; - } + this.logger.warn(`Ignoring WebFinger response for ${chalk.magenta(acct)}: remote URI points to a local user.`); + return null; } - this.logger.info(`Fetching new remote user ${chalk.magenta(acctLower)} from ${self.href}`); + this.logger.info(`Fetching new remote user ${chalk.magenta(acct)} from ${self.href}`); return await this.apPersonService.createPerson(self.href); + } catch (err) { + this.logger.warn(`Failed to resolve user ${acct}: ${renderInlineError(err)}`); + return null; + } + } + + @bindThis + private async tryUpdateUser(user: MiRemoteUser, acctLower: string): Promise { + // Don't update unless the user is at least 24 hours outdated. + // ユーザー情報が古い場合は、WebFingerからやりなおして返す + if (user.lastFetchedAt != null && Date.now() - user.lastFetchedAt.getTime() <= 1000 * 60 * 60 * 24) { + return user; } - // ユーザー情報が古い場合は、WebFingerからやりなおして返す - if (user.lastFetchedAt == null || this.timeService.now - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { - // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する - await this.usersRepository.update(user.id, { - lastFetchedAt: this.timeService.date, - }); + // Always mark as updated so we don't get stuck here for missing remote users. + // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する + await this.usersRepository.update(user.id, { + lastFetchedAt: new Date(), + }); + try { + // Resolve via webfinger const self = await this.resolveSelf(acctLower); - if (user.uri !== self.href) { - // if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping. - this.logger.warn(`Detected URI mismatch for ${acctLower}`); - - // validate uri - const uriHost = this.utilityService.extractDbHost(self.href); - if (uriHost !== host) { - throw new Error(`Failed to correct URI for ${acctLower}: new URI ${self.href} has different host from previous URI ${user.uri}`); - } - - await this.usersRepository.update({ - usernameLower, - host: host, - }, { - uri: self.href, - }); - await this.apPersonService.uriPersonCache.delete(user.uri); // Unmap the old URI - } - - this.logger.info(`Corrected URI for ${acctLower} from ${user.uri} to ${self.href}`); - + // Update the user + await this.tryUpdateUri(user, acctLower, self.href); await this.apPersonService.updatePerson(self.href); - - return await this.usersRepository.findOneByOrFail({ uri: self.href }) as MiLocalUser | MiRemoteUser; + } catch (err) { + this.logger.warn(`Could not update user ${acctLower}; will continue with outdated local copy: ${renderInlineError(err)}`); } - return user; + // Reload user + return await this.cacheService.findRemoteUserById(user.id); + } + + @bindThis + private async tryUpdateUri(user: MiRemoteUser, acct: string, href: string): Promise { + // Only update if there's actually a mismatch + if (user.uri === href) { + return; + } + + // if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping. + this.logger.warn(`Detected URI mismatch for ${acct}`); + + // validate uri + const uriHost = this.utilityService.extractDbHost(href); + if (uriHost !== user.host) { + throw new Error(`Failed to correct URI for ${acct}: new URI ${href} has different host from previous URI ${user.uri}`); + } + + // Update URI + await this.usersRepository.update({ id: user.id }, { uri: href }); // Update the user + await this.cacheService.uriPersonCache.delete(user.uri); // Unmap the old URI + await this.internalEventService.emit('remoteUserUpdated', { id: user.id }); // Update caches + + this.logger.info(`Corrected URI for ${acct} from ${user.uri} to ${href}`); } @bindThis From f6e1b40fd81aa5759d7535d1eb6de8e9c18c701d Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 16 Sep 2025 11:27:59 -0400 Subject: [PATCH 07/16] fix exception when fetching a user who doesn't exist yet --- packages/backend/src/core/activitypub/models/ApPersonService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 927ce9e574..69c69431d0 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -306,7 +306,7 @@ export class ApPersonService implements OnModuleInit { if (parsed.local) { userId = parsed.type === 'users' ? parsed.id : null; } else { - userId = await this.cacheService.uriPersonCache.fetch(uri); + userId = await this.cacheService.uriPersonCache.fetch(uri).catch(() => null); } // No match From 0f2e8092290dd2de279b0a1b2859d07aacaef95e Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 16 Sep 2025 11:28:26 -0400 Subject: [PATCH 08/16] fix timer leak in SkRateLimiterService --- packages/backend/src/server/SkRateLimiterService.ts | 2 ++ .../backend/test/unit/server/api/SkRateLimiterServiceTests.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/backend/src/server/SkRateLimiterService.ts b/packages/backend/src/server/SkRateLimiterService.ts index 212f00c86a..3212e5b4bd 100644 --- a/packages/backend/src/server/SkRateLimiterService.ts +++ b/packages/backend/src/server/SkRateLimiterService.ts @@ -13,6 +13,7 @@ import { RoleService } from '@/core/RoleService.js'; import { CacheManagementService, type ManagedMemoryKVCache } from '@/global/CacheManagementService.js'; import { ConflictError } from '@/misc/errors/ConflictError.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; // Sentinel value used for caching the default role template. // Required because MemoryKVCache doesn't support null keys. @@ -64,6 +65,7 @@ export class SkRateLimiterService { * @param limit The limit definition * @param actorOrUser authenticated client user or IP hash */ + @bindThis public async limit(limit: Keyed, actorOrUser: string | MiUser): Promise { if (this.disabled) { return disabledLimitInfo; diff --git a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts index c8e22c5e3c..e9aff39f06 100644 --- a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts +++ b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts @@ -67,6 +67,10 @@ describe(SkRateLimiterService, () => { mockEnvService.env.NODE_ENV = 'production'; }); + afterEach(() => { + serviceUnderTest().dispose(); + }); + describe('limit', () => { const actor = 'actor'; const key = 'test'; From a0b2ef780b46dca772feb038412ea0ed0695d1d1 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 17 Sep 2025 13:06:20 -0400 Subject: [PATCH 09/16] fix users not updating after resolving via webfinger --- .../backend/src/core/RemoteUserResolveService.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index 3860ceb15b..434edcfd0c 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -103,12 +103,6 @@ export class RemoteUserResolveService { return user; } - // Always mark as updated so we don't get stuck here for missing remote users. - // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する - await this.usersRepository.update(user.id, { - lastFetchedAt: new Date(), - }); - try { // Resolve via webfinger const self = await this.resolveSelf(acctLower); @@ -118,6 +112,12 @@ export class RemoteUserResolveService { await this.apPersonService.updatePerson(self.href); } catch (err) { this.logger.warn(`Could not update user ${acctLower}; will continue with outdated local copy: ${renderInlineError(err)}`); + } finally { + // Always mark as updated so we don't get stuck here for missing remote users. + // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する + await this.usersRepository.update(user.id, { + lastFetchedAt: new Date(), + }); } // Reload user From e1044879ea3be8fd6d737216370c0ea1d74fc8f6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 6 Oct 2025 11:41:44 -0400 Subject: [PATCH 10/16] fix merge errors --- .../backend/src/core/RemoteUserResolveService.ts | 6 ++++-- .../backend/src/server/SkRateLimiterService.ts | 14 +++++++++++++- .../unit/server/api/SkRateLimiterServiceTests.ts | 13 ------------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index 434edcfd0c..e84fd9dfe2 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -20,6 +20,7 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { bindThis } from '@/decorators.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; import { CacheService } from '@/core/CacheService.js'; +import { TimeService } from '@/core/TimeService.js'; import * as Acct from '@/misc/acct.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { InternalEventService } from '@/core/InternalEventService.js'; @@ -43,6 +44,7 @@ export class RemoteUserResolveService { private apPersonService: ApPersonService, private readonly cacheService: CacheService, private readonly internalEventService: InternalEventService, + private readonly timeService: TimeService, ) { this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user'); this.selfHost = this.utilityService.toPuny(this.config.host); @@ -99,7 +101,7 @@ export class RemoteUserResolveService { private async tryUpdateUser(user: MiRemoteUser, acctLower: string): Promise { // Don't update unless the user is at least 24 hours outdated. // ユーザー情報が古い場合は、WebFingerからやりなおして返す - if (user.lastFetchedAt != null && Date.now() - user.lastFetchedAt.getTime() <= 1000 * 60 * 60 * 24) { + if (user.lastFetchedAt != null && this.timeService.now - user.lastFetchedAt.getTime() <= 1000 * 60 * 60 * 24) { return user; } @@ -116,7 +118,7 @@ export class RemoteUserResolveService { // Always mark as updated so we don't get stuck here for missing remote users. // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する await this.usersRepository.update(user.id, { - lastFetchedAt: new Date(), + lastFetchedAt: this.timeService.now, }); } diff --git a/packages/backend/src/server/SkRateLimiterService.ts b/packages/backend/src/server/SkRateLimiterService.ts index 3212e5b4bd..377da6605d 100644 --- a/packages/backend/src/server/SkRateLimiterService.ts +++ b/packages/backend/src/server/SkRateLimiterService.ts @@ -8,7 +8,19 @@ import Redis from 'ioredis'; import type { MiUser } from '@/models/_.js'; import { TimeService } from '@/global/TimeService.js'; import { EnvService } from '@/global/EnvService.js'; -import { BucketRateLimit, LegacyRateLimit, LimitInfo, RateLimit, hasMinLimit, isLegacyRateLimit, Keyed, hasMaxLimit, disabledLimitInfo, MaxLegacyLimit, MinLegacyLimit } from '@/misc/rate-limit-utils.js'; +import { + type BucketRateLimit, + type LegacyRateLimit, + type LimitInfo, + type RateLimit, + type Keyed, + type MaxLegacyLimit, + type MinLegacyLimit, + hasMinLimit, + isLegacyRateLimit, + hasMaxLimit, + disabledLimitInfo, +} from '@/misc/rate-limit-utils.js'; import { RoleService } from '@/core/RoleService.js'; import { CacheManagementService, type ManagedMemoryKVCache } from '@/global/CacheManagementService.js'; import { ConflictError } from '@/misc/errors/ConflictError.js'; diff --git a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts index e9aff39f06..a8f88d2454 100644 --- a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts +++ b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts @@ -58,19 +58,6 @@ describe(SkRateLimiterService, () => { }; }); - afterEach(() => { - cacheManagementService.dispose(); - mockInternalEventService.mockReset(); - mockRedis.mockReset(); - - mockEnvService.mockReset(); - mockEnvService.env.NODE_ENV = 'production'; - }); - - afterEach(() => { - serviceUnderTest().dispose(); - }); - describe('limit', () => { const actor = 'actor'; const key = 'test'; From 18b909a10c2e4e96283a1cfda1d528a53b34fdf2 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 6 Oct 2025 11:41:57 -0400 Subject: [PATCH 11/16] move uriPersonCache to ApPersonService --- packages/backend/src/core/RemoteUserResolveService.ts | 2 +- packages/backend/src/core/activitypub/models/ApPersonService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index e84fd9dfe2..6d5af05716 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -144,7 +144,7 @@ export class RemoteUserResolveService { // Update URI await this.usersRepository.update({ id: user.id }, { uri: href }); // Update the user - await this.cacheService.uriPersonCache.delete(user.uri); // Unmap the old URI + await this.apPersonService.uriPersonCache.delete(user.uri); // Unmap the old URI await this.internalEventService.emit('remoteUserUpdated', { id: user.id }); // Update caches this.logger.info(`Corrected URI for ${acct} from ${user.uri} to ${href}`); diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 69c69431d0..184ce92694 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -306,7 +306,7 @@ export class ApPersonService implements OnModuleInit { if (parsed.local) { userId = parsed.type === 'users' ? parsed.id : null; } else { - userId = await this.cacheService.uriPersonCache.fetch(uri).catch(() => null); + userId = await this.uriPersonCache.fetch(uri).catch(() => null); } // No match From 0757ca921f79436709ee3ea059fd89791a8977c1 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 6 Oct 2025 11:50:59 -0400 Subject: [PATCH 12/16] rename cacheService.getUsers to findUsers to match naming scheme --- packages/backend/src/server/api/endpoints/users/show.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 3fcfa8490a..1733c5e460 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -116,7 +116,7 @@ export default class extends Endpoint { // eslint- return []; } - const users = await this.cacheService.getUsers(ps.userIds); + const users = await this.cacheService.findUsersById(ps.userIds); // リクエストされた通りに並べ替え // 順番は保持されるけど数は減ってる可能性がある From ac324655c3621e81cfd6f9f8cf7e6a25c1b26d4c Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 22 Oct 2025 22:42:43 -0400 Subject: [PATCH 13/16] fix attempt to set lastFetchedAt to a number --- packages/backend/src/core/RemoteUserResolveService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index 6d5af05716..688b9d7b1f 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -118,7 +118,7 @@ export class RemoteUserResolveService { // Always mark as updated so we don't get stuck here for missing remote users. // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する await this.usersRepository.update(user.id, { - lastFetchedAt: this.timeService.now, + lastFetchedAt: this.timeService.date, }); } From 1fc337f9d080321a7ae88ff91c9a808b48e361bc Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 6 Nov 2025 23:55:01 -0500 Subject: [PATCH 14/16] fix rebase errors --- packages/backend/src/core/RemoteUserResolveService.ts | 4 ++-- packages/backend/src/core/UtilityService.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index 688b9d7b1f..6a0d28dcdf 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -20,10 +20,10 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { bindThis } from '@/decorators.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; import { CacheService } from '@/core/CacheService.js'; -import { TimeService } from '@/core/TimeService.js'; +import { TimeService } from '@/global/TimeService.js'; import * as Acct from '@/misc/acct.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { InternalEventService } from '@/core/InternalEventService.js'; +import { InternalEventService } from '@/global/InternalEventService.js'; @Injectable() export class RemoteUserResolveService { diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 63b16a55d8..4a8145bddd 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -15,6 +15,7 @@ import type { MiMeta, SoftwareSuspension } from '@/models/Meta.js'; import type { MiInstance } from '@/models/Instance.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { EnvService } from '@/global/EnvService.js'; +import { getApId, type IObject } from '@/core/activitypub/type.js'; export type UriParseResult = { /** wether the URI was generated by us */ From 5c0424239a175b7f132dcdccfe3af9d9baeb17f3 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 7 Nov 2025 00:02:15 -0500 Subject: [PATCH 15/16] fix cache errors in SkRateLimiterServiceTests.ts --- .../test/unit/server/api/SkRateLimiterServiceTests.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts index a8f88d2454..c8e22c5e3c 100644 --- a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts +++ b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts @@ -58,6 +58,15 @@ describe(SkRateLimiterService, () => { }; }); + afterEach(() => { + cacheManagementService.dispose(); + mockInternalEventService.mockReset(); + mockRedis.mockReset(); + + mockEnvService.mockReset(); + mockEnvService.env.NODE_ENV = 'production'; + }); + describe('limit', () => { const actor = 'actor'; const key = 'test'; From bfcfa969a0cf4b276c39e96efde5dffb5da5536b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 7 Nov 2025 01:43:33 -0500 Subject: [PATCH 16/16] rearrange imports in RemoteUserResolveService to avoid merge conflicts --- packages/backend/src/core/RemoteUserResolveService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index 6a0d28dcdf..3d21cb92a9 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -11,19 +11,19 @@ import type { UsersRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import { isRemoteUser } from '@/models/User.js'; import { UtilityService } from '@/core/UtilityService.js'; import { ILink, WebfingerService } from '@/core/WebfingerService.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; -import { bindThis } from '@/decorators.js'; -import { renderInlineError } from '@/misc/render-inline-error.js'; -import { CacheService } from '@/core/CacheService.js'; import { TimeService } from '@/global/TimeService.js'; -import * as Acct from '@/misc/acct.js'; +import { CacheService } from '@/core/CacheService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { InternalEventService } from '@/global/InternalEventService.js'; +import * as Acct from '@/misc/acct.js'; +import { isRemoteUser } from '@/models/User.js'; +import { bindThis } from '@/decorators.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; @Injectable() export class RemoteUserResolveService {