activitypub: refactor to always apply recursion limit

Refactor to remove as many "new Resolver" as possible.
This commit is contained in:
Johann150 2022-12-04 00:29:45 +01:00
parent c4211761e6
commit a421dd401c
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
18 changed files with 48 additions and 61 deletions

View file

@ -137,6 +137,6 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
}); });
// アクティビティを処理 // アクティビティを処理
await perform(authUser.user, activity); await perform(authUser.user, activity, resolver);
return 'ok'; return 'ok';
}; };

View file

@ -4,13 +4,11 @@ import Resolver from '@/remote/activitypub/resolver.js';
import { IAccept, isFollow, getApType } from '@/remote/activitypub/type.js'; import { IAccept, isFollow, getApType } from '@/remote/activitypub/type.js';
import acceptFollow from './follow.js'; import acceptFollow from './follow.js';
export default async (actor: CacheableRemoteUser, activity: IAccept): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IAccept, resolver: Resolver): Promise<string> => {
const uri = activity.id || activity; const uri = activity.id || activity;
apLogger.info(`Accept: ${uri}`); apLogger.info(`Accept: ${uri}`);
const resolver = new Resolver();
const object = await resolver.resolve(activity.object).catch(e => { const object = await resolver.resolve(activity.object).catch(e => {
apLogger.error(`Resolution failed: ${e}`); apLogger.error(`Resolution failed: ${e}`);
throw e; throw e;

View file

@ -4,13 +4,11 @@ import Resolver from '@/remote/activitypub/resolver.js';
import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; import { IAnnounce, getApId } from '@/remote/activitypub/type.js';
import announceNote from './note.js'; import announceNote from './note.js';
export default async (actor: CacheableRemoteUser, activity: IAnnounce): Promise<void> => { export default async (actor: CacheableRemoteUser, activity: IAnnounce, resolver: Resolver): Promise<void> => {
const uri = getApId(activity); const uri = getApId(activity);
apLogger.info(`Announce: ${uri}`); apLogger.info(`Announce: ${uri}`);
const resolver = new Resolver();
const targetUri = getApId(activity.object); const targetUri = getApId(activity.object);
announceNote(resolver, actor, activity, targetUri); announceNote(resolver, actor, activity, targetUri);

View file

@ -5,7 +5,7 @@ import { ICreate, getApId, isPost, getApType } from '../../type.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
import createNote from './note.js'; import createNote from './note.js';
export default async (actor: CacheableRemoteUser, activity: ICreate): Promise<void> => { export default async (actor: CacheableRemoteUser, activity: ICreate, resolver: Resolver): Promise<void> => {
const uri = getApId(activity); const uri = getApId(activity);
apLogger.info(`Create: ${uri}`); apLogger.info(`Create: ${uri}`);
@ -26,8 +26,6 @@ export default async (actor: CacheableRemoteUser, activity: ICreate): Promise<vo
activity.object.attributedTo = activity.actor; activity.object.attributedTo = activity.actor;
} }
const resolver = new Resolver();
const object = await resolver.resolve(activity.object).catch(e => { const object = await resolver.resolve(activity.object).catch(e => {
apLogger.error(`Resolution failed: ${e}`); apLogger.error(`Resolution failed: ${e}`);
throw e; throw e;

View file

@ -18,13 +18,12 @@ import remove from './remove/index.js';
import block from './block/index.js'; import block from './block/index.js';
import flag from './flag/index.js'; import flag from './flag/index.js';
export async function performActivity(actor: CacheableRemoteUser, activity: IObject) { export async function performActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver) {
if (isCollectionOrOrderedCollection(activity)) { if (isCollectionOrOrderedCollection(activity)) {
const resolver = new Resolver();
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 performOneActivity(actor, act); await performOneActivity(actor, act, resolver);
} catch (err) { } catch (err) {
if (err instanceof Error || typeof err === 'string') { if (err instanceof Error || typeof err === 'string') {
apLogger.error(err); apLogger.error(err);
@ -32,37 +31,37 @@ export async function performActivity(actor: CacheableRemoteUser, activity: IObj
} }
} }
} else { } else {
await performOneActivity(actor, activity); await performOneActivity(actor, activity, resolver);
} }
} }
async function performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise<void> { async function performOneActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
if (actor.isSuspended) return; if (actor.isSuspended) return;
if (isCreate(activity)) { if (isCreate(activity)) {
await create(actor, activity); await create(actor, activity, resolver);
} else if (isDelete(activity)) { } else if (isDelete(activity)) {
await performDeleteActivity(actor, activity); await performDeleteActivity(actor, activity);
} else if (isUpdate(activity)) { } else if (isUpdate(activity)) {
await performUpdateActivity(actor, activity); await performUpdateActivity(actor, activity, resolver);
} else if (isRead(activity)) { } else if (isRead(activity)) {
await performReadActivity(actor, activity); await performReadActivity(actor, activity);
} else if (isFollow(activity)) { } else if (isFollow(activity)) {
await follow(actor, activity); await follow(actor, activity);
} else if (isAccept(activity)) { } else if (isAccept(activity)) {
await accept(actor, activity); await accept(actor, activity, resolver);
} else if (isReject(activity)) { } else if (isReject(activity)) {
await reject(actor, activity); await reject(actor, activity, resolver);
} else if (isAdd(activity)) { } else if (isAdd(activity)) {
await add(actor, activity).catch(err => apLogger.error(err)); await add(actor, activity).catch(err => apLogger.error(err));
} else if (isRemove(activity)) { } else if (isRemove(activity)) {
await remove(actor, activity).catch(err => apLogger.error(err)); await remove(actor, activity).catch(err => apLogger.error(err));
} else if (isAnnounce(activity)) { } else if (isAnnounce(activity)) {
await announce(actor, activity); await announce(actor, activity, resolver);
} else if (isLike(activity)) { } else if (isLike(activity)) {
await like(actor, activity); await like(actor, activity);
} else if (isUndo(activity)) { } else if (isUndo(activity)) {
await undo(actor, activity); await undo(actor, activity, resolver);
} else if (isBlock(activity)) { } else if (isBlock(activity)) {
await block(actor, activity); await block(actor, activity);
} else if (isFlag(activity)) { } else if (isFlag(activity)) {

View file

@ -4,13 +4,11 @@ import { IReject, isFollow, getApType } from '../../type.js';
import Resolver from '../../resolver.js'; import Resolver from '../../resolver.js';
import rejectFollow from './follow.js'; import rejectFollow from './follow.js';
export default async (actor: CacheableRemoteUser, activity: IReject): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IReject, resolver: Resolver): Promise<string> => {
const uri = activity.id || activity; const uri = activity.id || activity;
apLogger.info(`Reject: ${uri}`); apLogger.info(`Reject: ${uri}`);
const resolver = new Resolver();
const object = await resolver.resolve(activity.object).catch(e => { const object = await resolver.resolve(activity.object).catch(e => {
apLogger.error(`Resolution failed: ${e}`); apLogger.error(`Resolution failed: ${e}`);
throw e; throw e;

View file

@ -8,7 +8,7 @@ import undoLike from './like.js';
import undoAccept from './accept.js'; import undoAccept from './accept.js';
import { undoAnnounce } from './announce.js'; import { undoAnnounce } from './announce.js';
export default async (actor: CacheableRemoteUser, activity: IUndo): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IUndo, resolver: Resolver): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.actor) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }
@ -17,7 +17,6 @@ export default async (actor: CacheableRemoteUser, activity: IUndo): Promise<stri
apLogger.info(`Undo: ${uri}`); apLogger.info(`Undo: ${uri}`);
const resolver = new Resolver();
const object = await resolver.resolve(activity.object).catch(e => { const object = await resolver.resolve(activity.object).catch(e => {
apLogger.error(`Resolution failed: ${e}`); apLogger.error(`Resolution failed: ${e}`);
throw e; throw e;

View file

@ -8,15 +8,13 @@ import { updatePerson } from '@/remote/activitypub/models/person.js';
/** /**
* Updateアクティビティを捌きます * Updateアクティビティを捌きます
*/ */
export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IUpdate, resolver: Resolver): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.actor) { if ('actor' in activity && actor.uri !== activity.actor) {
return 'skip: invalid actor'; return 'skip: invalid actor';
} }
apLogger.debug('Update'); apLogger.debug('Update');
const resolver = new Resolver();
const object = await resolver.resolve(activity.object).catch(e => { const object = await resolver.resolve(activity.object).catch(e => {
apLogger.error(`Resolution failed: ${e}`); apLogger.error(`Resolution failed: ${e}`);
throw e; throw e;

View file

@ -31,7 +31,7 @@ function authUserFromApId(uri: string): Promise<AuthUser | null> {
export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise<AuthUser | null> { export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise<AuthUser | null> {
let authUser = await publicKeyCache.fetch(keyId) let authUser = await publicKeyCache.fetch(keyId)
.then(key => { .then(async key => {
if (!key) return null; if (!key) return null;
else return { else return {
user: await userByIdCache.fetch(key.userId), user: await userByIdCache.fetch(key.userId),

View file

@ -11,13 +11,13 @@ import { apLogger } from '../logger.js';
/** /**
* Imageを作成します * Imageを作成します
*/ */
export async function createImage(actor: CacheableRemoteUser, value: any): Promise<DriveFile> { export async function createImage(actor: CacheableRemoteUser, value: any, resolver: Resolver): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
throw new Error('actor has been suspended'); throw new Error('actor has been suspended');
} }
const image = await new Resolver().resolve(value) as any; const image = await resolver.resolve(value) as any;
if (image.url == null) { if (image.url == null) {
throw new Error('invalid image: url not privided'); throw new Error('invalid image: url not privided');
@ -58,9 +58,9 @@ export async function createImage(actor: CacheableRemoteUser, value: any): Promi
* If the target Image is registered in FoundKey, return it; otherwise, fetch it from the remote server and return it. * If the target Image is registered in FoundKey, return it; otherwise, fetch it from the remote server and return it.
* Fetch the image from the remote server, register it in FoundKey and return it. * Fetch the image from the remote server, register it in FoundKey and return it.
*/ */
export async function resolveImage(actor: CacheableRemoteUser, value: any): Promise<DriveFile> { export async function resolveImage(actor: CacheableRemoteUser, value: any, resolver: Resolver): Promise<DriveFile> {
// TODO // TODO
// リモートサーバーからフェッチしてきて登録 // Fetch from remote server and register it.
return await createImage(actor, value); return await createImage(actor, value, resolver);
} }

View file

@ -5,11 +5,9 @@ import { IObject, isMention, IApMention } from '../type.js';
import Resolver from '../resolver.js'; import Resolver from '../resolver.js';
import { resolvePerson } from './person.js'; import { resolvePerson } from './person.js';
export async function extractApMentions(tags: IObject | IObject[] | null | undefined) { export async function extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) {
const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
const resolver = new Resolver();
const limit = promiseLimit<CacheableUser | null>(2); const limit = promiseLimit<CacheableUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))), hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))),

View file

@ -63,7 +63,7 @@ export async function fetchNote(object: string | IObject): Promise<Note | null>
/** /**
* Noteを作成します * Noteを作成します
*/ */
export async function createNote(value: string | IObject, resolver?: Resolver = new Resolver(), silent = false): Promise<Note | null> { export async function createNote(value: string | IObject, resolver: Resolver, silent = false): Promise<Note | null> {
const object: any = await resolver.resolve(value); const object: any = await resolver.resolve(value);
const entryUri = getApId(value); const entryUri = getApId(value);
@ -107,7 +107,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver =
let isTalk = note._misskey_talk && visibility === 'specified'; let isTalk = note._misskey_talk && visibility === 'specified';
const apMentions = await extractApMentions(note.tag); const apMentions = await extractApMentions(note.tag, resolver);
const apHashtags = await extractApHashtags(note.tag); const apHashtags = await extractApHashtags(note.tag);
// 添付ファイル // 添付ファイル
@ -119,7 +119,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver =
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
const files = note.attachment const files = note.attachment
.map(attach => attach.sensitive = note.sensitive) .map(attach => attach.sensitive = note.sensitive)
? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<DriveFile>))) ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x, resolver)) as Promise<DriveFile>)))
.filter(image => image != null) .filter(image => image != null)
: []; : [];

View file

@ -131,7 +131,7 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise<Cac
/** /**
* Personを作成します * Personを作成します
*/ */
export async function createPerson(uri: string, resolver?: Resolver = new Resolver()): Promise<User> { export async function createPerson(uri: string, resolver: Resolver): Promise<User> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
if (uri.startsWith(config.url)) { if (uri.startsWith(config.url)) {
@ -238,7 +238,7 @@ export async function createPerson(uri: string, resolver?: Resolver = new Resolv
].map(img => ].map(img =>
img == null img == null
? Promise.resolve(null) ? Promise.resolve(null)
: resolveImage(user!, img).catch(() => null), : resolveImage(user!, img, resolver).catch(() => null),
)); ));
const avatarId = avatar ? avatar.id : null; const avatarId = avatar ? avatar.id : null;
@ -278,7 +278,7 @@ export async function createPerson(uri: string, resolver?: Resolver = new Resolv
* @param resolver Resolver * @param resolver Resolver
* @param hint Hint of Person object (If this value is a valid Person, it is used for updating without Remote resolve.) * @param hint Hint of Person object (If this value is a valid Person, it is used for updating without Remote resolve.)
*/ */
export async function updatePerson(uri: string, resolver?: Resolver = new Resolver(), hint?: IObject): Promise<void> { export async function updatePerson(uri: string, resolver: Resolver, hint?: IObject): Promise<void> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
@ -307,7 +307,7 @@ export async function updatePerson(uri: string, resolver?: Resolver = new Resolv
].map(img => ].map(img =>
img == null img == null
? Promise.resolve(null) ? Promise.resolve(null)
: resolveImage(exist, img).catch(() => null), : resolveImage(exist, img, resolver).catch(() => null),
)); ));
// カスタム絵文字取得 // カスタム絵文字取得
@ -386,7 +386,7 @@ export async function updatePerson(uri: string, resolver?: Resolver = new Resolv
* If the target Person is registered in FoundKey, return it; otherwise, fetch it from a remote server and return it. * If the target Person is registered in FoundKey, return it; otherwise, fetch it from a remote server and return it.
* Fetch the person from the remote server, register it in FoundKey, and return it. * Fetch the person from the remote server, register it in FoundKey, and return it.
*/ */
export async function resolvePerson(uri: string, resolver?: Resolver): Promise<CacheableUser> { export async function resolvePerson(uri: string, resolver: Resolver): Promise<CacheableUser> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
@ -398,7 +398,7 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise<C
//#endregion //#endregion
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
return await createPerson(uri, resolver ?? new Resolver()); return await createPerson(uri, resolver);
} }
const services: { const services: {
@ -455,15 +455,13 @@ export function analyzeAttachments(attachments: IObject | IObject[] | undefined)
return { fields, services }; return { fields, services };
} }
export async function updateFeatured(userId: User['id'], resolver?: Resolver) { async function updateFeatured(userId: User['id'], resolver: Resolver) {
const user = await Users.findOneByOrFail({ id: userId }); const user = await Users.findOneByOrFail({ id: userId });
if (!Users.isRemoteUser(user)) return; if (!Users.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;
apLogger.info(`Updating the featured: ${user.uri}`); apLogger.info(`Updating the featured: ${user.uri}`);
if (resolver == null) resolver = new Resolver();
// Resolve to (Ordered)Collection Object // Resolve to (Ordered)Collection Object
const collection = await resolver.resolveCollection(user.featured); const collection = await resolver.resolveCollection(user.featured);
if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection');

View file

@ -5,7 +5,7 @@ import Resolver from '../resolver.js';
import { IObject, IQuestion, isQuestion } from '../type.js'; import { IObject, IQuestion, isQuestion } from '../type.js';
import { apLogger } from '../logger.js'; import { apLogger } from '../logger.js';
export async function extractPollFromQuestion(source: string | IObject, resolver?: Resolver = new Resolver()): Promise<IPoll> { export async function extractPollFromQuestion(source: string | IObject, resolver: Resolver): Promise<IPoll> {
const question = await resolver.resolve(source); const question = await resolver.resolve(source);
if (!isQuestion(question)) { if (!isQuestion(question)) {
@ -39,7 +39,7 @@ export async function extractPollFromQuestion(source: string | IObject, resolver
* @param resolver Resolver to use * @param resolver Resolver to use
* @returns true if updated * @returns true if updated
*/ */
export async function updateQuestion(value: string | IObject, resolver?: Resolver = new Resolver()) { export async function updateQuestion(value: string | IObject, resolver: Resolver) {
const uri = typeof value === 'string' ? value : value.id; const uri = typeof value === 'string' ? value : value.id;
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ

View file

@ -3,15 +3,16 @@ import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IObject } from './type.js'; import { IObject } from './type.js';
import { performActivity } from './kernel/index.js'; import { performActivity } from './kernel/index.js';
import { updatePerson } from './models/person.js'; import { updatePerson } from './models/person.js';
import Resolver from './resolver.js';
export default async (actor: CacheableRemoteUser, activity: IObject): Promise<void> => { export default async (actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> => {
await performActivity(actor, activity); await performActivity(actor, activity, resolver);
// And while I'm at it, I'll update the remote user information if it's out of date. // And while I'm at it, I'll update the remote user information if it's out of date.
if (actor.uri) { if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > DAY) { if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > DAY) {
setImmediate(() => { setImmediate(() => {
updatePerson(actor.uri!); updatePerson(actor.uri!, resolver);
}); });
} }
} }

View file

@ -9,10 +9,11 @@ import { Users } from '@/models/index.js';
import webFinger from './webfinger.js'; import webFinger from './webfinger.js';
import { createPerson, updatePerson } from './activitypub/models/person.js'; import { createPerson, updatePerson } from './activitypub/models/person.js';
import { remoteLogger } from './logger.js'; import { remoteLogger } from './logger.js';
import Resolver from './activitypub/resolver.js';
const logger = remoteLogger.createSubLogger('resolve-user'); const logger = remoteLogger.createSubLogger('resolve-user');
export async function resolveUser(username: string, idnHost: string | null): Promise<User> { export async function resolveUser(username: string, idnHost: string | null, resolver: Resolver = new Resolver()): Promise<User> {
const usernameLower = username.toLowerCase(); const usernameLower = username.toLowerCase();
if (idnHost == null) { if (idnHost == null) {
@ -47,7 +48,7 @@ export async function resolveUser(username: string, idnHost: string | null): Pro
const self = await resolveSelf(acctLower); const self = await resolveSelf(acctLower);
logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
return await createPerson(self); return await createPerson(self, resolver);
} }
// If user information is out of date, start over with webfinger // If user information is out of date, start over with webfinger
@ -81,7 +82,7 @@ export async function resolveUser(username: string, idnHost: string | null): Pro
logger.info(`uri is fine: ${acctLower}`); logger.info(`uri is fine: ${acctLower}`);
} }
await updatePerson(self); await updatePerson(self, resolver);
logger.info(`return resynced remote user: ${acctLower}`); logger.info(`return resynced remote user: ${acctLower}`);
return await Users.findOneBy({ uri: self }).then(u => { return await Users.findOneBy({ uri: self }).then(u => {

View file

@ -114,8 +114,8 @@ async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined):
return await mergePack( return await mergePack(
me, me,
isActor(object) ? await createPerson(getApId(object)) : null, isActor(object) ? await createPerson(getApId(object), resolver) : null,
isPost(object) ? await createNote(getApId(object), undefined, true) : null, isPost(object) ? await createNote(getApId(object), resolver, true) : null,
); );
} }

View file

@ -1,3 +1,4 @@
import Resolver from '@/remote/activitypub/resolver.js';
import { updatePerson } from '@/remote/activitypub/models/person.js'; import { updatePerson } from '@/remote/activitypub/models/person.js';
import define from '../../define.js'; import define from '../../define.js';
import { getRemoteUser } from '../../common/getters.js'; import { getRemoteUser } from '../../common/getters.js';
@ -19,5 +20,5 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps) => { export default define(meta, paramDef, async (ps) => {
const user = await getRemoteUser(ps.userId); const user = await getRemoteUser(ps.userId);
await updatePerson(user.uri!); await updatePerson(user.uri!, new Resolver());
}); });