diff --git a/src/prelude/array.ts b/src/prelude/array.ts index 44482c57c..839bbc920 100644 --- a/src/prelude/array.ts +++ b/src/prelude/array.ts @@ -120,3 +120,11 @@ export function cumulativeSum(xs: number[]): number[] { export function fromEntries(xs: [string, any][]): { [x: string]: any; } { return xs.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {} as { [x: string]: any; }); } + +export function toArray(x: T | T[] | undefined): T[] { + return Array.isArray(x) ? x : x != null ? [x] : []; +} + +export function toSingle(x: T | T[] | undefined): T | undefined { + return Array.isArray(x) ? x[0] : x; +} diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts index 68fce52e1..a9447840b 100644 --- a/src/remote/activitypub/kernel/announce/index.ts +++ b/src/remote/activitypub/kernel/announce/index.ts @@ -1,7 +1,7 @@ import Resolver from '../../resolver'; import { IRemoteUser } from '../../../../models/entities/user'; import announceNote from './note'; -import { IAnnounce, INote, validPost, getApId } from '../../type'; +import { IAnnounce, validPost, getApId } from '../../type'; import { apLogger } from '../../logger'; const logger = apLogger; @@ -23,7 +23,7 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise => } if (validPost.includes(object.type)) { - announceNote(resolver, actor, activity, object as INote); + announceNote(resolver, actor, activity, object); } else { logger.warn(`Unknown announce type: ${object.type}`); } diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts index f0594a57b..a5db5b8ca 100644 --- a/src/remote/activitypub/kernel/announce/note.ts +++ b/src/remote/activitypub/kernel/announce/note.ts @@ -1,7 +1,7 @@ import Resolver from '../../resolver'; import post from '../../../../services/note/create'; import { IRemoteUser, User } from '../../../../models/entities/user'; -import { IAnnounce, INote, getApId, getApIds } from '../../type'; +import { IAnnounce, IObject, getApId, getApIds } from '../../type'; import { fetchNote, resolveNote } from '../../models/note'; import { resolvePerson } from '../../models/person'; import { apLogger } from '../../logger'; @@ -14,7 +14,7 @@ const logger = apLogger; /** * アナウンスアクティビティを捌きます */ -export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise { +export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: IObject): Promise { const uri = getApId(activity); // アナウンサーが凍結されていたらスキップ diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts index 5c247326c..24bc9d524 100644 --- a/src/remote/activitypub/kernel/block/index.ts +++ b/src/remote/activitypub/kernel/block/index.ts @@ -1,5 +1,5 @@ import config from '../../../../config'; -import { IBlock } from '../../type'; +import { IBlock, getApId } from '../../type'; import block from '../../../../services/blocking/create'; import { apLogger } from '../../logger'; import { Users } from '../../../../models'; @@ -8,10 +8,9 @@ import { IRemoteUser } from '../../../../models/entities/user'; const logger = apLogger; export default async (actor: IRemoteUser, activity: IBlock): Promise => { - const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - if (id == null) throw new Error('missing id'); + const id = getApId(activity.object); - const uri = activity.id || activity; + const uri = getApId(activity); logger.info(`Block: ${uri}`); diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts index a28eaa11f..6ccaa17ef 100644 --- a/src/remote/activitypub/kernel/create/note.ts +++ b/src/remote/activitypub/kernel/create/note.ts @@ -1,13 +1,13 @@ import Resolver from '../../resolver'; import { IRemoteUser } from '../../../../models/entities/user'; import { createNote, fetchNote } from '../../models/note'; -import { getApId } from '../../type'; +import { getApId, IObject } from '../../type'; import { getApLock } from '../../../../misc/app-lock'; /** * 投稿作成アクティビティを捌きます */ -export default async function(resolver: Resolver, actor: IRemoteUser, note: any, silent = false): Promise { +export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false): Promise { const uri = getApId(note); const unlock = await getApLock(uri); diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts index a0646bdd6..c8298dc79 100644 --- a/src/remote/activitypub/kernel/index.ts +++ b/src/remote/activitypub/kernel/index.ts @@ -1,4 +1,4 @@ -import { Object } from '../type'; +import { IObject, isCreate, isDelete, isUpdate, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection } from '../type'; import { IRemoteUser } from '../../../models/entities/user'; import create from './create'; import performDeleteActivity from './delete'; @@ -13,68 +13,53 @@ import add from './add'; import remove from './remove'; import block from './block'; import { apLogger } from '../logger'; +import Resolver from '../resolver'; +import { toArray } from '../../../prelude/array'; -const self = async (actor: IRemoteUser, activity: Object): Promise => { +export async function performActivity(actor: IRemoteUser, activity: IObject) { + if (isCollectionOrOrderedCollection(activity)) { + const resolver = new Resolver(); + for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { + const act = await resolver.resolve(item); + try { + await performOneActivity(actor, act); + } catch (e) { + apLogger.error(e); + } + } + } else { + await performOneActivity(actor, activity); + } +} + +async function performOneActivity(actor: IRemoteUser, activity: IObject): Promise { if (actor.isSuspended) return; - switch (activity.type) { - case 'Create': + if (isCreate(activity)) { await create(actor, activity); - break; - - case 'Delete': + } else if (isDelete(activity)) { await performDeleteActivity(actor, activity); - break; - - case 'Update': + } else if (isUpdate(activity)) { await performUpdateActivity(actor, activity); - break; - - case 'Follow': + } else if (isFollow(activity)) { await follow(actor, activity); - break; - - case 'Accept': + } else if (isAccept(activity)) { await accept(actor, activity); - break; - - case 'Reject': + } else if (isReject(activity)) { await reject(actor, activity); - break; - - case 'Add': + } else if (isAdd(activity)) { await add(actor, activity).catch(err => apLogger.error(err)); - break; - - case 'Remove': + } else if (isRemove(activity)) { await remove(actor, activity).catch(err => apLogger.error(err)); - break; - - case 'Announce': + } else if (isAnnounce(activity)) { await announce(actor, activity); - break; - - case 'Like': + } else if (isLike(activity)) { await like(actor, activity); - break; - - case 'Undo': + } else if (isUndo(activity)) { await undo(actor, activity); - break; - - case 'Block': + } else if (isBlock(activity)) { await block(actor, activity); - break; - - case 'Collection': - case 'OrderedCollection': - // TODO - break; - - default: + } else { apLogger.warn(`unknown activity type: ${(activity as any).type}`); - return; } -}; - -export default self; +} diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index 780a05d26..a0b951c5f 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -26,6 +26,8 @@ import { UserProfile } from '../../../models/entities/user-profile'; import { validActor } from '../../../remote/activitypub/type'; import { getConnection } from 'typeorm'; import { ensure } from '../../../prelude/ensure'; +import { toArray } from '../../../prelude/array'; + const logger = apLogger; /** @@ -463,8 +465,7 @@ export async function updateFeatured(userId: User['id']) { // Resolve to Object(may be Note) arrays const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; - const items = await resolver.resolve(unresolvedItems); - if (!Array.isArray(items)) throw new Error(`Collection items is not an array`); + const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x))); // Resolve and regist Notes const limit = promiseLimit(2); diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts index 425adaec9..12e72fdea 100644 --- a/src/remote/activitypub/perform.ts +++ b/src/remote/activitypub/perform.ts @@ -1,7 +1,7 @@ -import { Object } from './type'; +import { IObject } from './type'; import { IRemoteUser } from '../../models/entities/user'; -import kernel from './kernel'; +import { performActivity } from './kernel'; -export default async (actor: IRemoteUser, activity: Object): Promise => { - await kernel(actor, activity); +export default async (actor: IRemoteUser, activity: IObject): Promise => { + await performActivity(actor, activity); }; diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index d656c1c5e..5b8224453 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -1,5 +1,5 @@ import * as request from 'request-promise-native'; -import { IObject } from './type'; +import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type'; import config from '../../config'; export default class Resolver { @@ -14,31 +14,19 @@ export default class Resolver { return Array.from(this.history); } - public async resolveCollection(value: any) { + public async resolveCollection(value: string | IObject): Promise { const collection = typeof value === 'string' ? await this.resolve(value) : value; - switch (collection.type) { - case 'Collection': { - collection.objects = collection.items; - break; - } - - case 'OrderedCollection': { - collection.objects = collection.orderedItems; - break; - } - - default: { - throw new Error(`unknown collection type: ${collection.type}`); - } + if (isCollectionOrOrderedCollection(collection)) { + return collection; + } else { + throw new Error(`unknown collection type: ${collection.type}`); } - - return collection; } - public async resolve(value: any): Promise { + public async resolve(value: string | IObject): Promise { if (value == null) { throw new Error('resolvee is null (or undefined)'); } diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index bc9d14190..62475faef 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -1,4 +1,5 @@ export type obj = { [x: string]: any }; +export type ApObject = IObject | string | (IObject | string)[]; export interface IObject { '@context': string | obj | obj[]; @@ -6,9 +7,9 @@ export interface IObject { id?: string; summary?: string; published?: string; - cc?: IObject | string | (IObject | string)[]; - to?: IObject | string | (IObject | string)[]; - attributedTo: IObject | string | (IObject | string)[]; + cc?: ApObject; + to?: ApObject; + attributedTo: ApObject; attachment?: any[]; inReplyTo?: any; replies?: ICollection; @@ -26,7 +27,7 @@ export interface IObject { /** * Get array of ActivityStreams Objects id */ -export function getApIds(value: IObject | string | (IObject | string)[] | undefined): string[] { +export function getApIds(value: ApObject | undefined): string[] { if (value == null) return []; const array = Array.isArray(value) ? value : [value]; return array.map(x => getApId(x)); @@ -35,7 +36,7 @@ export function getApIds(value: IObject | string | (IObject | string)[] | undefi /** * Get first ActivityStreams Object id */ -export function getOneApId(value: IObject | string | (IObject | string)[]): string { +export function getOneApId(value: ApObject): string { const firstOne = Array.isArray(value) ? value[0] : value; return getApId(firstOne); } @@ -59,13 +60,13 @@ export interface IActivity extends IObject { export interface ICollection extends IObject { type: 'Collection'; totalItems: number; - items: IObject | string | IObject[] | string[]; + items: ApObject; } export interface IOrderedCollection extends IObject { type: 'OrderedCollection'; totalItems: number; - orderedItems: IObject | string | IObject[] | string[]; + orderedItems: ApObject; } export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video']; @@ -170,18 +171,15 @@ export interface IBlock extends IActivity { type: 'Block'; } -export type Object = - ICollection | - IOrderedCollection | - ICreate | - IDelete | - IUpdate | - IUndo | - IFollow | - IAccept | - IReject | - IAdd | - IRemove | - ILike | - IAnnounce | - IBlock; +export const isCreate = (object: IObject): object is ICreate => object.type === 'Create'; +export const isDelete = (object: IObject): object is IDelete => object.type === 'Delete'; +export const isUpdate = (object: IObject): object is IUpdate => object.type === 'Update'; +export const isUndo = (object: IObject): object is IUndo => object.type === 'Undo'; +export const isFollow = (object: IObject): object is IFollow => object.type === 'Follow'; +export const isAccept = (object: IObject): object is IAccept => object.type === 'Accept'; +export const isReject = (object: IObject): object is IReject => object.type === 'Reject'; +export const isAdd = (object: IObject): object is IAdd => object.type === 'Add'; +export const isRemove = (object: IObject): object is IRemove => object.type === 'Remove'; +export const isLike = (object: IObject): object is ILike => object.type === 'Like'; +export const isAnnounce = (object: IObject): object is IAnnounce => object.type === 'Announce'; +export const isBlock = (object: IObject): object is IBlock => object.type === 'Block';