refactor(federation): Inboxのエラーハンドリングの仕様変更 (#13610)

Cherry-picked from 89b27d8587

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
tamaina 2024-05-28 14:36:06 +09:00 committed by あわわわとーにゅ
parent 303a52160c
commit 25e24b053a
No known key found for this signature in database
GPG key ID: 6AFBBF529601C1DB
4 changed files with 84 additions and 59 deletions

View file

@ -38,7 +38,7 @@ import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js'; import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js'; import { ApQuestionService } from './models/ApQuestionService.js';
import type { Resolver } from './ApResolverService.js'; import type { Resolver } from './ApResolverService.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js'; import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
@Injectable() @Injectable()
export class ApInboxService { export class ApInboxService {
@ -90,13 +90,15 @@ export class ApInboxService {
} }
@bindThis @bindThis
public async performActivity(actor: MiRemoteUser, activity: IObject, additionalCc?: MiLocalUser['id']): Promise<void> { public async performActivity(actor: MiRemoteUser, activity: IObject, additionalCc?: MiLocalUser['id']): Promise<string> {
let result = 'error';
if (isCollectionOrOrderedCollection(activity)) { if (isCollectionOrOrderedCollection(activity)) {
const results = [] as [string, string | void][];
const resolver = this.apResolverService.createResolver(); const resolver = this.apResolverService.createResolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
const act = await resolver.resolve(item); const act = await resolver.resolve(item);
try { try {
await this.performOneActivity(actor, act, additionalCc); results.push([getApId(item), await this.performOneActivity(actor, act, additionalCc)]);
} catch (err) { } catch (err) {
if (err instanceof Error || typeof err === 'string') { if (err instanceof Error || typeof err === 'string') {
this.logger.error(err); this.logger.error(err);
@ -105,8 +107,13 @@ export class ApInboxService {
} }
} }
} }
const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok')));
if (hasReason) {
result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
}
} else { } else {
await this.performOneActivity(actor, activity, additionalCc); result = await this.performOneActivity(actor, activity, additionalCc);
} }
// ついでにリモートユーザーの情報が古かったら更新しておく // ついでにリモートユーザーの情報が古かったら更新しておく
@ -117,42 +124,44 @@ export class ApInboxService {
}); });
} }
} }
return result;
} }
@bindThis @bindThis
public async performOneActivity(actor: MiRemoteUser, activity: IObject, additionalCc?: MiLocalUser['id']): Promise<void> { public async performOneActivity(actor: MiRemoteUser, activity: IObject, additionalCc?: MiLocalUser['id']): Promise<string> {
if (actor.isSuspended) return; if (actor.isSuspended) return 'skip: actor is suspended';
if (isCreate(activity)) { if (isCreate(activity)) {
await this.create(actor, activity, additionalCc); return await this.create(actor, activity, additionalCc);
} else if (isDelete(activity)) { } else if (isDelete(activity)) {
await this.delete(actor, activity); return await this.delete(actor, activity);
} else if (isUpdate(activity)) { } else if (isUpdate(activity)) {
await this.update(actor, activity, additionalCc); return await this.update(actor, activity, additionalCc);
} else if (isFollow(activity)) { } else if (isFollow(activity)) {
await this.follow(actor, activity); return await this.follow(actor, activity);
} else if (isAccept(activity)) { } else if (isAccept(activity)) {
await this.accept(actor, activity); return await this.accept(actor, activity);
} else if (isReject(activity)) { } else if (isReject(activity)) {
await this.reject(actor, activity); return await this.reject(actor, activity);
} else if (isAdd(activity)) { } else if (isAdd(activity)) {
await this.add(actor, activity).catch(err => this.logger.error(err)); return await this.add(actor, activity).catch(err => { this.logger.error(err); return `error: ${err.message}`; });
} else if (isRemove(activity)) { } else if (isRemove(activity)) {
await this.remove(actor, activity).catch(err => this.logger.error(err)); return await this.remove(actor, activity).catch(err => { this.logger.error(err); return `error: ${err.message}`; });
} else if (isAnnounce(activity)) { } else if (isAnnounce(activity)) {
await this.announce(actor, activity); return await this.announce(actor, activity);
} else if (isLike(activity)) { } else if (isLike(activity)) {
await this.like(actor, activity); return await this.like(actor, activity);
} else if (isUndo(activity)) { } else if (isUndo(activity)) {
await this.undo(actor, activity); return await this.undo(actor, activity);
} else if (isBlock(activity)) { } else if (isBlock(activity)) {
await this.block(actor, activity); return await this.block(actor, activity);
} else if (isFlag(activity)) { } else if (isFlag(activity)) {
await this.flag(actor, activity); return await this.flag(actor, activity);
} else if (isMove(activity)) { } else if (isMove(activity)) {
await this.move(actor, activity); return await this.move(actor, activity);
} else { } else {
this.logger.warn(`unrecognized activity type: ${activity.type}`); return `skip: unknown activity type ${activity.type}`;
} }
} }
@ -234,47 +243,56 @@ export class ApInboxService {
} }
@bindThis @bindThis
private async add(actor: MiRemoteUser, activity: IAdd): Promise<void> { private async add(actor: MiRemoteUser, activity: IAdd): Promise<string> {
if (actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); return 'skip: invalid actor';
} }
if (activity.target == null) { if (activity.target == null) {
throw new Error('target is null'); return 'skip: target is null';
} }
if (activity.target === actor.featured) { if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object); const note = await this.apNoteService.resolveNote(activity.object);
if (note == null) throw new Error('note not found'); if (note == null) return 'skip: note not found';
await this.notePiningService.addPinned(actor, note.id); await this.notePiningService.addPinned(actor, note.id);
return; return 'ok';
} }
throw new Error(`unknown target: ${activity.target}`); return `skip: unknown target ${activity.target}`;
} }
@bindThis @bindThis
private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<void> { private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<string> {
const uri = getApId(activity); const uri = getApId(activity);
this.logger.info(`Announce: ${uri}`); this.logger.info(`Announce: ${uri}`);
const targetUri = getApId(activity.object); const resolver = this.apResolverService.createResolver();
await this.announceNote(actor, activity, targetUri); if (!activity.object) return 'skip: activity has no object property';
const target = await resolver.resolve(activity.object).catch(err => {
this.logger.error(`Resolution failed: ${err}`, { error: err });
return err;
});
if (isPost(target)) await this.announceNote(actor, activity, target);
return `skip: unknown object type ${getApType(target)}`;
} }
@bindThis @bindThis
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> { private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise<string> {
const uri = getApId(activity); const uri = getApId(activity);
if (actor.isSuspended) { if (actor.isSuspended) {
return; return 'skip: actor is suspended';
} }
// アナウンス先をブロックしてたら中断 // アナウンス先をブロックしてたら中断
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return 'skip: blocked host';
const unlock = await this.appLockService.getApLock(uri); const unlock = await this.appLockService.getApLock(uri);
@ -282,30 +300,28 @@ export class ApInboxService {
// 既に同じURIを持つものが登録されていないかチェック // 既に同じURIを持つものが登録されていないかチェック
const exist = await this.apNoteService.fetchNote(uri); const exist = await this.apNoteService.fetchNote(uri);
if (exist) { if (exist) {
return; return 'skip: note exists';
} }
// Announce対象をresolve // Announce対象をresolve
let renote; let renote;
try { try {
renote = await this.apNoteService.resolveNote(targetUri); renote = await this.apNoteService.resolveNote(target);
if (renote == null) throw new Error('announce target is null'); if (renote == null) return 'skip: target note not found';
} catch (err) { } catch (err) {
// 対象が4xxならスキップ // 対象が4xxならスキップ
if (err instanceof StatusError) { if (err instanceof StatusError) {
if (!err.isRetryable) { if (!err.isRetryable) {
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`); return `skip: Ignored announce target ${target} - ${err.statusCode}`;
return;
} }
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`); this.logger.warn(`Error in announce target ${target} - ${err.statusCode}`);
} }
throw err; throw err;
} }
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) { if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
this.logger.warn('skip: invalid actor for this activity'); return 'skip: invalid actor for this activity';
return;
} }
this.logger.info(`Creating the (Re)Note: ${uri}`); this.logger.info(`Creating the (Re)Note: ${uri}`);
@ -314,8 +330,7 @@ export class ApInboxService {
const createdAt = activity.published ? new Date(activity.published) : null; const createdAt = activity.published ? new Date(activity.published) : null;
if (createdAt && createdAt < this.idService.parse(renote.id).date) { if (createdAt && createdAt < this.idService.parse(renote.id).date) {
this.logger.warn('skip: malformed createdAt'); return 'skip: malformed createdAt';
return;
} }
await this.noteCreateService.create(actor, { await this.noteCreateService.create(actor, {
@ -328,6 +343,8 @@ export class ApInboxService {
} finally { } finally {
unlock(); unlock();
} }
return 'ok';
} }
@bindThis @bindThis
@ -349,11 +366,13 @@ export class ApInboxService {
} }
@bindThis @bindThis
private async create(actor: MiRemoteUser, activity: ICreate, additionalCc?: MiLocalUser['id']): Promise<void> { private async create(actor: MiRemoteUser, activity: ICreate, additionalCc?: MiLocalUser['id']): Promise<string> {
const uri = getApId(activity); const uri = getApId(activity);
this.logger.info(`Create: ${uri}`); this.logger.info(`Create: ${uri}`);
if (!activity.object) return 'skip: activity has no object property';
// copy audiences between activity <=> object. // copy audiences between activity <=> object.
if (typeof activity.object === 'object') { if (typeof activity.object === 'object') {
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
@ -378,9 +397,9 @@ export class ApInboxService {
}); });
if (isPost(object)) { if (isPost(object)) {
await this.createNote(resolver, actor, object, false, activity, additionalCc); return await this.createNote(resolver, actor, object, false, activity, additionalCc);
} else { } else {
this.logger.warn(`Unknown type: ${getApType(object)}`); return `skip: Unknown type ${getApType(object)}`;
} }
} }
@ -433,7 +452,7 @@ export class ApInboxService {
@bindThis @bindThis
private async delete(actor: MiRemoteUser, activity: IDelete): Promise<string> { private async delete(actor: MiRemoteUser, activity: IDelete): Promise<string> {
if (actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); return 'skip: invalid actor';
} }
// 削除対象objectのtype // 削除対象objectのtype
@ -595,29 +614,29 @@ export class ApInboxService {
} }
@bindThis @bindThis
private async remove(actor: MiRemoteUser, activity: IRemove): Promise<void> { private async remove(actor: MiRemoteUser, activity: IRemove): Promise<string> {
if (actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); return 'skip: invalid actor';
} }
if (activity.target == null) { if (activity.target == null) {
throw new Error('target is null'); return 'skip: target is null';
} }
if (activity.target === actor.featured) { if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object); const note = await this.apNoteService.resolveNote(activity.object);
if (note == null) throw new Error('note not found'); if (note == null) return 'skip: note not found';
await this.notePiningService.removePinned(actor, note.id); await this.notePiningService.removePinned(actor, note.id);
return; return 'ok';
} }
throw new Error(`unknown target: ${activity.target}`); return `skip: unknown target ${activity.target}`;
} }
@bindThis @bindThis
private async undo(actor: MiRemoteUser, activity: IUndo): Promise<string> { private async undo(actor: MiRemoteUser, activity: IUndo): Promise<string> {
if (actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); return 'skip: invalid actor';
} }
const uri = activity.id ?? activity; const uri = activity.id ?? activity;

View file

@ -84,20 +84,20 @@ export class ApNoteService {
const expectHost = this.utilityService.extractDbHost(uri); const expectHost = this.utilityService.extractDbHost(uri);
if (!validPost.includes(getApType(object))) { if (!validPost.includes(getApType(object))) {
return new Error(`invalid Note: invalid object type ${getApType(object)}`); return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
} }
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
} }
const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo)); const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
if (object.attributedTo && actualHost !== expectHost) { if (object.attributedTo && actualHost !== expectHost) {
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
} }
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
return new Error('invalid Note: published timestamp is malformed'); return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
} }
return null; return null;

View file

@ -328,3 +328,4 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move'; export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';

View file

@ -198,7 +198,11 @@ export class InboxProcessorService implements OnApplicationShutdown {
// アクティビティを処理 // アクティビティを処理
try { try {
await this.apInboxService.performActivity(authUser.user, activity, job.data.user?.id); const result = await this.apInboxService.performActivity(authUser.user, activity, job.data.user?.id);
if (result && !result.startsWith('ok')) {
this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`);
return result;
}
} catch (e) { } catch (e) {
if (e instanceof IdentifiableError) { if (e instanceof IdentifiableError) {
if ([ if ([
@ -206,6 +210,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
'689ee33f-f97c-479a-ac49-1b9f8140af99', '689ee33f-f97c-479a-ac49-1b9f8140af99',
'9f466dab-c856-48cd-9e65-ff90ff750580', '9f466dab-c856-48cd-9e65-ff90ff750580',
'85ab9bd7-3a41-4530-959d-f07073900109', '85ab9bd7-3a41-4530-959d-f07073900109',
'd450b8a9-48e4-4dab-ae36-f4db763fda7c',
].includes(e.id)) return e.message; ].includes(e.id)) return e.message;
} }
throw e; throw e;