From caf40e40fbd444c050d8d3cc60f4dd2898950900 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Mon, 31 May 2021 13:04:13 +0900 Subject: [PATCH] Supports Array ActivityStreams type (#7536) * Supports Array type * Fix * Fix Service to Note * Update type.ts Co-authored-by: syuilo --- src/remote/activitypub/kernel/accept/index.ts | 14 ++--- src/remote/activitypub/kernel/create/index.ts | 6 +- src/remote/activitypub/kernel/reject/index.ts | 14 ++--- .../activitypub/kernel/undo/announce.ts | 5 +- src/remote/activitypub/kernel/undo/index.ts | 26 +++----- src/remote/activitypub/kernel/update/index.ts | 8 +-- src/remote/activitypub/models/note.ts | 6 +- src/remote/activitypub/models/person.ts | 8 +-- src/remote/activitypub/type.ts | 61 ++++++++++++------- src/server/api/endpoints/ap/show.ts | 10 +-- 10 files changed, 76 insertions(+), 82 deletions(-) diff --git a/src/remote/activitypub/kernel/accept/index.ts b/src/remote/activitypub/kernel/accept/index.ts index 083e312a6f..79cdbb2ef7 100644 --- a/src/remote/activitypub/kernel/accept/index.ts +++ b/src/remote/activitypub/kernel/accept/index.ts @@ -1,12 +1,12 @@ import Resolver from '../../resolver'; import { IRemoteUser } from '../../../../models/entities/user'; import acceptFollow from './follow'; -import { IAccept, IFollow } from '../../type'; +import { IAccept, isFollow, getApType } from '../../type'; import { apLogger } from '../../logger'; const logger = apLogger; -export default async (actor: IRemoteUser, activity: IAccept): Promise => { +export default async (actor: IRemoteUser, activity: IAccept): Promise => { const uri = activity.id || activity; logger.info(`Accept: ${uri}`); @@ -18,13 +18,7 @@ export default async (actor: IRemoteUser, activity: IAccept): Promise => { throw e; }); - switch (object.type) { - case 'Follow': - acceptFollow(actor, object as IFollow); - break; + if (isFollow(object)) return await acceptFollow(actor, object); - default: - logger.warn(`Unknown accept type: ${object.type}`); - break; - } + return `skip: Unknown Accept type: ${getApType(object)}`; }; diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts index 108cfedf41..f1a3ebff43 100644 --- a/src/remote/activitypub/kernel/create/index.ts +++ b/src/remote/activitypub/kernel/create/index.ts @@ -1,7 +1,7 @@ import Resolver from '../../resolver'; import { IRemoteUser } from '../../../../models/entities/user'; import createNote from './note'; -import { ICreate, getApId, validPost } from '../../type'; +import { ICreate, getApId, isPost, getApType } from '../../type'; import { apLogger } from '../../logger'; import { toArray, concat, unique } from '../../../../prelude/array'; @@ -35,9 +35,9 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise => { throw e; }); - if (validPost.includes(object.type)) { + if (isPost(object)) { createNote(resolver, actor, object, false, activity); } else { - logger.warn(`Unknown type: ${object.type}`); + logger.warn(`Unknown type: ${getApType(object)}`); } }; diff --git a/src/remote/activitypub/kernel/reject/index.ts b/src/remote/activitypub/kernel/reject/index.ts index 96e9aadf5d..d7a80fce7b 100644 --- a/src/remote/activitypub/kernel/reject/index.ts +++ b/src/remote/activitypub/kernel/reject/index.ts @@ -1,12 +1,12 @@ import Resolver from '../../resolver'; import { IRemoteUser } from '../../../../models/entities/user'; import rejectFollow from './follow'; -import { IReject, IFollow } from '../../type'; +import { IReject, isFollow, getApType } from '../../type'; import { apLogger } from '../../logger'; const logger = apLogger; -export default async (actor: IRemoteUser, activity: IReject): Promise => { +export default async (actor: IRemoteUser, activity: IReject): Promise => { const uri = activity.id || activity; logger.info(`Reject: ${uri}`); @@ -18,13 +18,7 @@ export default async (actor: IRemoteUser, activity: IReject): Promise => { throw e; }); - switch (object.type) { - case 'Follow': - rejectFollow(actor, object as IFollow); - break; + if (isFollow(object)) return await rejectFollow(actor, object); - default: - logger.warn(`Unknown reject type: ${object.type}`); - break; - } + return `skip: Unknown Reject type: ${getApType(object)}`; }; diff --git a/src/remote/activitypub/kernel/undo/announce.ts b/src/remote/activitypub/kernel/undo/announce.ts index 38ce5b6c59..e08fea188d 100644 --- a/src/remote/activitypub/kernel/undo/announce.ts +++ b/src/remote/activitypub/kernel/undo/announce.ts @@ -3,14 +3,15 @@ import { IRemoteUser } from '../../../../models/entities/user'; import { IAnnounce, getApId } from '../../type'; import deleteNote from '../../../../services/note/delete'; -export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise => { +export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise => { const uri = getApId(activity); const note = await Notes.findOne({ uri }); - if (!note) return; + if (!note) return 'skip: no such Announce'; await deleteNote(actor, note); + return 'ok: deleted'; }; diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts index 93909352d9..0bab3c9666 100644 --- a/src/remote/activitypub/kernel/undo/index.ts +++ b/src/remote/activitypub/kernel/undo/index.ts @@ -1,5 +1,5 @@ import { IRemoteUser } from '../../../../models/entities/user'; -import { IUndo, IFollow, IBlock, ILike, IAnnounce } from '../../type'; +import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type'; import unfollow from './follow'; import unblock from './block'; import undoLike from './like'; @@ -9,7 +9,7 @@ import { apLogger } from '../../logger'; const logger = apLogger; -export default async (actor: IRemoteUser, activity: IUndo): Promise => { +export default async (actor: IRemoteUser, activity: IUndo): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -25,20 +25,10 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise => { throw e; }); - switch (object.type) { - case 'Follow': - unfollow(actor, object as IFollow); - break; - case 'Block': - unblock(actor, object as IBlock); - break; - case 'Like': - case 'EmojiReaction': - case 'EmojiReact': - undoLike(actor, object as ILike); - break; - case 'Announce': - undoAnnounce(actor, object as IAnnounce); - break; - } + if (isFollow(object)) return await unfollow(actor, object); + if (isBlock(object)) return await unblock(actor, object); + if (isLike(object)) return await undoLike(actor, object); + if (isAnnounce(object)) return await undoAnnounce(actor, object); + + return `skip: unknown object type ${getApType(object)}`; }; diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts index ea7e6a063e..6dd3e5f296 100644 --- a/src/remote/activitypub/kernel/update/index.ts +++ b/src/remote/activitypub/kernel/update/index.ts @@ -1,5 +1,5 @@ import { IRemoteUser } from '../../../../models/entities/user'; -import { IUpdate, validActor } from '../../type'; +import { getApType, IUpdate, isActor } from '../../type'; import { apLogger } from '../../logger'; import { updateQuestion } from '../../models/question'; import Resolver from '../../resolver'; @@ -22,13 +22,13 @@ export default async (actor: IRemoteUser, activity: IUpdate): Promise => throw e; }); - if (validActor.includes(object.type)) { + if (isActor(object)) { await updatePerson(actor.uri!, resolver, object); return `ok: Person updated`; - } else if (object.type === 'Question') { + } else if (getApType(object) === 'Question') { await updateQuestion(object).catch(e => console.log(e)); return `ok: Question updated`; } else { - return `skip: Unknown type: ${object.type}`; + return `skip: Unknown type: ${getApType(object)}`; } }; diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 09e066708f..3b7452c3cb 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update'; import { extractDbHost, toPuny } from '@/misc/convert-host'; import { Emojis, Polls, MessagingMessages } from '../../../models'; import { Note } from '../../../models/entities/note'; -import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji } from '../type'; +import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type'; import { Emoji } from '../../../models/entities/emoji'; import { genId } from '@/misc/gen-id'; import { fetchMeta } from '@/misc/fetch-meta'; @@ -36,8 +36,8 @@ export function validateNote(object: any, uri: string) { return new Error('invalid Note: object is null'); } - if (!validPost.includes(object.type)) { - return new Error(`invalid Note: invalid object type ${object.type}`); + if (!validPost.includes(getApType(object))) { + return new Error(`invalid Note: invalid object type ${getApType(object)}`); } if (object.id && extractDbHost(object.id) !== expectHost) { diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index 5b032d9d9c..1062fe2995 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -4,7 +4,7 @@ import * as promiseLimit from 'promise-limit'; import config from '@/config'; import Resolver from '../resolver'; import { resolveImage } from './image'; -import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue } from '../type'; +import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType } from '../type'; import { fromHtml } from '../../../mfm/from-html'; import { htmlToMfm } from '../misc/html-to-mfm'; import { resolveNote, extractEmojis } from './note'; @@ -137,7 +137,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise normalizeForSearch(tag)).splice(0, 32); - const isBot = object.type === 'Service'; + const isBot = getApType(object) === 'Service'; const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); @@ -337,7 +337,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint emojis: emojiNames, name: person.name, tags, - isBot: object.type === 'Service', + isBot: getApType(object) === 'Service', isCat: (person as any).isCat === true, isLocked: !!person.manuallyApprovesFollowers, isExplorable: !!person.discoverable, @@ -476,7 +476,7 @@ export async function updateFeatured(userId: User['id']) { // Resolve and regist Notes const limit = promiseLimit(2); const featuredNotes = await Promise.all(items - .filter(item => item.type === 'Note') + .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも .slice(0, 5) .map(item => limit(() => resolveNote(item, resolver)))); diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index db866ae67a..98025da908 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -3,7 +3,7 @@ export type ApObject = IObject | string | (IObject | string)[]; export interface IObject { '@context': string | obj | obj[]; - type: string; + type: string | unknown[]; id?: string; summary?: string; published?: string; @@ -51,6 +51,15 @@ export function getApId(value: string | IObject): string { throw new Error(`cannot detemine id`); } +/** + * Get ActivityStreams Object type + */ +export function getApType(value: IObject): string { + if (typeof value.type === 'string') return value.type; + if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; + throw new Error(`cannot detect type`); +} + export function getOneApHrefNullable(value: ApObject | undefined): string | undefined { const firstOne = Array.isArray(value) ? value[0] : value; return getApHrefNullable(firstOne); @@ -92,6 +101,9 @@ export interface IOrderedCollection extends IObject { export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event']; +export const isPost = (object: IObject): object is IPost => + validPost.includes(getApType(object)); + export interface IPost extends IObject { type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; _misskey_content?: string; @@ -112,7 +124,7 @@ export interface IQuestion extends IObject { } export const isQuestion = (object: IObject): object is IQuestion => - object.type === 'Note' || object.type === 'Question'; + getApType(object) === 'Note' || getApType(object) === 'Question'; interface IQuestionChoice { name?: string; @@ -126,10 +138,13 @@ export interface ITombstone extends IObject { } export const isTombstone = (object: IObject): object is ITombstone => - object.type === 'Tombstone'; + getApType(object) === 'Tombstone'; export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application']; +export const isActor = (object: IObject): object is IPerson => + validActor.includes(getApType(object)); + export interface IPerson extends IObject { type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application'; name?: string; @@ -154,10 +169,10 @@ export interface IPerson extends IObject { } export const isCollection = (object: IObject): object is ICollection => - object.type === 'Collection'; + getApType(object) === 'Collection'; export const isOrderedCollection = (object: IObject): object is IOrderedCollection => - object.type === 'OrderedCollection'; + getApType(object) === 'OrderedCollection'; export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => isCollection(object) || isOrderedCollection(object); @@ -171,7 +186,7 @@ export interface IApPropertyValue extends IObject { export const isPropertyValue = (object: IObject): object is IApPropertyValue => object && - object.type === 'PropertyValue' && + getApType(object) === 'PropertyValue' && typeof object.name === 'string' && typeof (object as any).value === 'string'; @@ -181,7 +196,7 @@ export interface IApMention extends IObject { } export const isMention = (object: IObject): object is IApMention=> - object.type === 'Mention' && + getApType(object) === 'Mention' && typeof object.href === 'string'; export interface IApHashtag extends IObject { @@ -190,7 +205,7 @@ export interface IApHashtag extends IObject { } export const isHashtag = (object: IObject): object is IApHashtag => - object.type === 'Hashtag' && + getApType(object) === 'Hashtag' && typeof object.name === 'string'; export interface IApEmoji extends IObject { @@ -199,7 +214,7 @@ export interface IApEmoji extends IObject { } export const isEmoji = (object: IObject): object is IApEmoji => - object.type === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; + getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; export interface ICreate extends IActivity { type: 'Create'; @@ -258,17 +273,17 @@ export interface IFlag extends IActivity { type: 'Flag'; } -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 isRead = (object: IObject): object is IRead => object.type === 'Read'; -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' || object.type === 'EmojiReaction' || object.type === 'EmojiReact'; -export const isAnnounce = (object: IObject): object is IAnnounce => object.type === 'Announce'; -export const isBlock = (object: IObject): object is IBlock => object.type === 'Block'; -export const isFlag = (object: IObject): object is IFlag => object.type === 'Flag'; +export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create'; +export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete'; +export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; +export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read'; +export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo'; +export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow'; +export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept'; +export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject'; +export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add'; +export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove'; +export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact'; +export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; +export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; +export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts index 2ce11160e8..b4df1ad4d7 100644 --- a/src/server/api/endpoints/ap/show.ts +++ b/src/server/api/endpoints/ap/show.ts @@ -10,7 +10,7 @@ import { Users, Notes } from '../../../../models'; import { Note } from '../../../../models/entities/note'; import { User } from '../../../../models/entities/user'; import { fetchMeta } from '@/misc/fetch-meta'; -import { validActor, validPost } from '../../../../remote/activitypub/type'; +import { isActor, isPost, getApId } from '../../../../remote/activitypub/type'; export const meta = { tags: ['federation'], @@ -154,16 +154,16 @@ async function fetchAny(uri: string) { } // それでもみつからなければ新規であるため登録 - if (validActor.includes(object.type)) { - const user = await createPerson(object.id); + if (isActor(object)) { + const user = await createPerson(getApId(object)); return { type: 'User', object: await Users.pack(user, null, { detail: true }) }; } - if (validPost.includes(object.type)) { - const note = await createNote(object.id, undefined, true); + if (isPost(object)) { + const note = await createNote(getApId(object), undefined, true); return { type: 'Note', object: await Notes.pack(note!, null, { detail: true })