make sure validation errors in background update-featured are returned to the job queue

This commit is contained in:
Hazelnoot 2025-09-17 12:25:24 -04:00
parent 3c64fbc9a5
commit e81c8b075b
5 changed files with 62 additions and 17 deletions

View file

@ -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<void> {
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<void> {
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}`);

View file

@ -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;

View file

@ -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';

View file

@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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);

View file

@ -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<void> {
async updatePersonLazy(uriOrUser: string | MiRemoteUser): Promise<void> {
const userId = typeof(uriOrUser) === 'object' ? uriOrUser.id : uriOrUser;
await this.updatePerson(userId, this.resolver);
}
@bindThis
async updateFeaturedLazy(userOrId: string | MiUser): Promise<void> {
await this.updateFeatured(userOrId, this.resolver);
async updateFeaturedLazy(userOrId: string | MiRemoteUser): Promise<void> {
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;
});
}
}