From e14509574d534a74fcebf9515146b028bbdec153 Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Fri, 27 Sep 2019 04:58:28 +0900
Subject: [PATCH] =?UTF-8?q?AP=20featured=20collection=E3=81=AE=E4=BF=AE?=
 =?UTF-8?q?=E6=AD=A3=20/=20Collection=20Activity=E3=81=AE=E5=AF=BE?=
 =?UTF-8?q?=E5=BF=9C=20/=20type=E3=81=AE=E4=BF=AE=E6=AD=A3=E3=81=AA?=
 =?UTF-8?q?=E3=81=A9=20(#5460)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* resolver type / fix updateFeatured

* type ApObject

* fix strange type

* AP Activity

* Collection Activityが失敗したらとりあえず無視
---
 src/prelude/array.ts                          |  8 ++
 .../activitypub/kernel/announce/index.ts      |  4 +-
 .../activitypub/kernel/announce/note.ts       |  4 +-
 src/remote/activitypub/kernel/block/index.ts  |  7 +-
 src/remote/activitypub/kernel/create/note.ts  |  4 +-
 src/remote/activitypub/kernel/index.ts        | 83 ++++++++-----------
 src/remote/activitypub/models/person.ts       |  5 +-
 src/remote/activitypub/perform.ts             |  8 +-
 src/remote/activitypub/resolver.ts            | 26 ++----
 src/remote/activitypub/type.ts                | 42 +++++-----
 10 files changed, 85 insertions(+), 106 deletions(-)

diff --git a/src/prelude/array.ts b/src/prelude/array.ts
index 44482c57cd..839bbc920b 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<T>(x: T | T[] | undefined): T[] {
+	return Array.isArray(x) ? x : x != null ? [x] : [];
+}
+
+export function toSingle<T>(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 68fce52e17..a9447840b7 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<void> =>
 	}
 
 	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 f0594a57b7..a5db5b8ca7 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<void> {
+export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: IObject): Promise<void> {
 	const uri = getApId(activity);
 
 	// アナウンサーが凍結されていたらスキップ
diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts
index 5c247326cb..24bc9d524f 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<void> => {
-	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 a28eaa11fb..6ccaa17ef5 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<void> {
+export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false): Promise<void> {
 	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 a0646bdd67..c8298dc797 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<void> => {
+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<void> {
 	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 780a05d262..a0b951c5f4 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<Note | null>(2);
diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts
index 425adaec96..12e72fdea5 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<void> => {
-	await kernel(actor, activity);
+export default async (actor: IRemoteUser, activity: IObject): Promise<void> => {
+	await performActivity(actor, activity);
 };
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index d656c1c5e5..5b82244536 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<ICollection | IOrderedCollection> {
 		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<IObject> {
+	public async resolve(value: string | IObject): Promise<IObject> {
 		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 bc9d14190a..62475faefc 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';