diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 7316dfa30a..f28f4cdd8d 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -44,7 +44,7 @@ import { TimeService } from '@/global/TimeService.js'; import { verifyFieldLinks } from '@/misc/verify-field-link.js'; import { isRetryableError } from '@/misc/is-retryable-error.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { errorCodes, IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueService } from '@/core/QueueService.js'; import { InternalEventService } from '@/core/InternalEventService.js'; import { CollapsedQueueService } from '@/core/CollapsedQueueService.js'; @@ -919,16 +919,30 @@ export class ApPersonService implements OnModuleInit { */ @bindThis public async updateFeaturedLazy(userOrId: MiRemoteUser | MiUser['id']): Promise { - const user = await this.resolveUserForFeatured(userOrId); - if (!user) return; + const userId = typeof(userOrId) === 'object' ? userOrId.id : userOrId; + const user = typeof(userOrId) === 'object' ? userOrId : await this.cacheService.findRemoteUserById(userId); - await this.queueService.createUpdateFeaturedJob(user.id); + if (user.isDeleted || user.isSuspended) { + this.logger.debug(`Not updating featured for ${userId}: user is deleted`); + return; + } + + if (!user.featured) { + this.logger.debug(`Not updating featured for ${userId}: no featured collection`); + return; + } + + await this.queueService.createUpdateFeaturedJob(userId); } @bindThis public async updateFeatured(userOrId: MiRemoteUser | MiUser['id'], resolver?: Resolver): Promise { - const user = await this.resolveUserForFeatured(userOrId); - if (!user) return; + const userId = typeof(userOrId) === 'object' ? userOrId.id : userOrId; + const user = typeof(userOrId) === 'object' ? userOrId : await this.cacheService.findRemoteUserById(userId); + + if (user.isDeleted) throw new IdentifiableError(errorCodes.userIsDeleted, `Can't update featured for ${userId}: user is deleted`); + if (user.isSuspended) throw new IdentifiableError(errorCodes.userIsSuspended, `Can't update featured for ${userId}: user is suspended`); + if (!user.featured) throw new IdentifiableError(errorCodes.noFeaturedCollection, `Can't update featured for ${userId}: no featured collection`); this.logger.info(`Updating featured notes for: ${user.uri}`); diff --git a/packages/backend/src/misc/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts index 9497791ea1..782c2efd67 100644 --- a/packages/backend/src/misc/identifiable-error.ts +++ b/packages/backend/src/misc/identifiable-error.ts @@ -25,3 +25,15 @@ export class IdentifiableError extends Error { this.isRetryable = isRetryable; } } + +/** + * Standard error codes to reference throughout the app + */ +export const errorCodes = { + // User has been deleted (hard or soft deleted) + userIsDeleted: '4cac9436-baa3-4955-a368-7628aea676cf', + // User is suspended (directly or by instance) + userIsSuspended: '1e56d624-737f-48e4-beb6-0bdddb9fa809', + // User has no valid featured collection (not defined, invalid, etc) + noFeaturedCollection: '2aa4766e-b7d8-4291-a671-56800498b085', +} as const; diff --git a/packages/backend/src/queue/processors/BackgroundTaskProcessorService.ts b/packages/backend/src/queue/processors/BackgroundTaskProcessorService.ts index b1b38ff788..eb7ebbe9a4 100644 --- a/packages/backend/src/queue/processors/BackgroundTaskProcessorService.ts +++ b/packages/backend/src/queue/processors/BackgroundTaskProcessorService.ts @@ -28,6 +28,8 @@ import { trackTask } from '@/misc/promise-tracker.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { ApLogService } from '@/core/ApLogService.js'; import { CollapsedQueueService } from '@/core/CollapsedQueueService.js'; +import { isRemoteUser } from '@/models/User.js'; +import { errorCodes, IdentifiableError } from '@/misc/identifiable-error.js'; @Injectable() export class BackgroundTaskProcessorService { @@ -110,7 +112,7 @@ export class BackgroundTaskProcessorService { const user = await this.cacheService.findOptionalUserById(task.userId); if (!user || user.isDeleted) return `Skipping update-user task: user ${task.userId} has been deleted`; if (user.isSuspended) return `Skipping update-user task: user ${task.userId} is suspended`; - if (!user.uri) return `Skipping update-user task: user ${task.userId} is local`; + if (!isRemoteUser(user)) return `Skipping update-user task: user ${task.userId} is local`; if (user.lastFetchedAt && Date.now() - user.lastFetchedAt.getTime() < 1000 * 60 * 60 * 24) { return `Skipping update-user task: user ${task.userId} was recently updated`; @@ -124,14 +126,23 @@ export class BackgroundTaskProcessorService { const user = await this.cacheService.findOptionalUserById(task.userId); if (!user || user.isDeleted) return `Skipping update-featured task: user ${task.userId} has been deleted`; if (user.isSuspended) return `Skipping update-featured task: user ${task.userId} is suspended`; - if (!user.uri) return `Skipping update-featured task: user ${task.userId} is local`; + if (!isRemoteUser(user)) return `Skipping update-featured task: user ${task.userId} is local`; if (!user.featured) return `Skipping update-featured task: user ${task.userId} has no featured collection`; if (user.lastFetchedAt && Date.now() - user.lastFetchedAt.getTime() < 1000 * 60 * 60 * 24) { return `Skipping update-featured task: user ${task.userId} was recently updated`; } - await this.apPersonService.updateFeatured(user); + try { + await this.apPersonService.updateFeatured(user); + } catch (err) { + if (err instanceof IdentifiableError) { + if (err.id === errorCodes.userIsSuspended) return err.message; + if (err.id === errorCodes.userIsDeleted) return err.message; + if (err.id === errorCodes.noFeaturedCollection) return err.message; + } + throw err; + } return 'ok'; } @@ -139,7 +150,7 @@ export class BackgroundTaskProcessorService { const user = await this.cacheService.findOptionalUserById(task.userId); if (!user || user.isDeleted) return `Skipping update-user-tags task: user ${task.userId} has been deleted`; if (user.isSuspended) return `Skipping update-user-tags task: user ${task.userId} is suspended`; - if (!user.uri) return `Skipping update-user-tags task: user ${task.userId} is local`; + if (!isRemoteUser(user)) return `Skipping update-user-tags task: user ${task.userId} is local`; await this.hashtagService.updateUsertags(user, user.tags); return 'ok'; diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts index 27c3e63ade..aec1d54fbe 100644 --- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts +++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts @@ -48,7 +48,7 @@ export default class extends Endpoint { // eslint- private readonly cacheService: CacheService, ) { super(meta, paramDef, async (ps) => { - const user = await this.cacheService.findRemoteUserById(ps.userId); + const user = await this.cacheService.findRemoteUserById(ps.userId).catch(() => null); if (!user) { throw new ApiError(meta.errors.noSuchUser); diff --git a/packages/backend/test/misc/immediateBackgroundTasks.ts b/packages/backend/test/misc/immediateBackgroundTasks.ts index 24c344da2f..a60a2cd7f8 100644 --- a/packages/backend/test/misc/immediateBackgroundTasks.ts +++ b/packages/backend/test/misc/immediateBackgroundTasks.ts @@ -3,25 +3,33 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import type { MiRemoteUser } from '@/models/User.js'; +import type { MiInstance } from '@/models/Instance.js'; +import type { Resolver } from '@/core/activitypub/ApResolverService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; -import { MiUser } from '@/models/User.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { MiInstance } from '@/models/Instance.js'; -import { Resolver } from '@/core/activitypub/ApResolverService.js'; import { bindThis } from '@/decorators.js'; +import { errorCodes, IdentifiableError } from '@/misc/identifiable-error.js'; export class ImmediateApPersonService extends ApPersonService { public resolver?: Resolver; @bindThis - async updatePersonLazy(uriOrUser: string | MiUser): Promise { + async updatePersonLazy(uriOrUser: string | MiRemoteUser): Promise { const userId = typeof(uriOrUser) === 'object' ? uriOrUser.id : uriOrUser; await this.updatePerson(userId, this.resolver); } @bindThis - async updateFeaturedLazy(userOrId: string | MiUser): Promise { - await this.updateFeatured(userOrId, this.resolver); + async updateFeaturedLazy(userOrId: string | MiRemoteUser): Promise { + await this.updateFeatured(userOrId, this.resolver).catch(err => { + if (err instanceof IdentifiableError) { + if (err.id === errorCodes.userIsSuspended) return; + if (err.id === errorCodes.userIsDeleted) return; + if (err.id === errorCodes.noFeaturedCollection) return; + } + throw err; + }); } }