diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts index 3ac490b00..03a50fece 100644 --- a/src/models/repositories/note.ts +++ b/src/models/repositories/note.ts @@ -144,8 +144,8 @@ export class NoteRepository extends Repository { let text = note.text; - if (note.name) { - text = `【${note.name}】\n${note.text}`; + if (note.name && note.uri) { + text = `【${note.name}】\n${(note.text || '').trim()}\n${note.uri}`; } const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)])); diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts index ebd5a27b9..68fce52e1 100644 --- a/src/remote/activitypub/kernel/announce/index.ts +++ b/src/remote/activitypub/kernel/announce/index.ts @@ -1,13 +1,13 @@ import Resolver from '../../resolver'; import { IRemoteUser } from '../../../../models/entities/user'; import announceNote from './note'; -import { IAnnounce, INote } from '../../type'; +import { IAnnounce, INote, validPost, getApId } from '../../type'; import { apLogger } from '../../logger'; const logger = apLogger; export default async (actor: IRemoteUser, activity: IAnnounce): Promise => { - const uri = activity.id || activity; + const uri = getApId(activity); logger.info(`Announce: ${uri}`); @@ -22,15 +22,9 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise => throw e; } - switch (object.type) { - case 'Note': - case 'Question': - case 'Article': + if (validPost.includes(object.type)) { announceNote(resolver, actor, activity, object as INote); - break; - - default: + } else { logger.warn(`Unknown announce type: ${object.type}`); - break; } }; diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts index 684304147..2a07f50c8 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 } from '../../type'; +import { IAnnounce, INote, getApId, getApIds } from '../../type'; import { fetchNote, resolveNote } from '../../models/note'; import { resolvePerson } from '../../models/person'; import { apLogger } from '../../logger'; @@ -14,17 +14,13 @@ const logger = apLogger; * アナウンスアクティビティを捌きます */ export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise { - const uri = activity.id || activity; + const uri = getApId(activity); // アナウンサーが凍結されていたらスキップ if (actor.isSuspended) { return; } - if (typeof uri !== 'string') { - throw new Error('invalid announce'); - } - // アナウンス先をブロックしてたら中断 const meta = await fetchMeta(); if (meta.blockedHosts.includes(extractDbHost(uri))) return; @@ -52,11 +48,14 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity: logger.info(`Creating the (Re)Note: ${uri}`); //#region Visibility - const visibility = getVisibility(activity.to || [], activity.cc || [], actor); + const to = getApIds(activity.to); + const cc = getApIds(activity.cc); + + const visibility = getVisibility(to, cc, actor); let visibleUsers: User[] = []; if (visibility == 'specified') { - visibleUsers = await Promise.all((note.to || []).map(uri => resolvePerson(uri))); + visibleUsers = await Promise.all(to.map(uri => resolvePerson(uri))); } //#endergion diff --git a/src/remote/activitypub/kernel/create/image.ts b/src/remote/activitypub/kernel/create/image.ts deleted file mode 100644 index 7720e8f1b..000000000 --- a/src/remote/activitypub/kernel/create/image.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IRemoteUser } from '../../../../models/entities/user'; -import { createImage } from '../../models/image'; - -export default async function(actor: IRemoteUser, image: any): Promise { - await createImage(image.url, actor); -} diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts index 0326b591f..a6fa2336f 100644 --- a/src/remote/activitypub/kernel/create/index.ts +++ b/src/remote/activitypub/kernel/create/index.ts @@ -1,14 +1,13 @@ import Resolver from '../../resolver'; import { IRemoteUser } from '../../../../models/entities/user'; -import createImage from './image'; import createNote from './note'; -import { ICreate } from '../../type'; +import { ICreate, getApId, validPost } from '../../type'; import { apLogger } from '../../logger'; const logger = apLogger; export default async (actor: IRemoteUser, activity: ICreate): Promise => { - const uri = activity.id || activity; + const uri = getApId(activity); logger.info(`Create: ${uri}`); @@ -23,19 +22,9 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise => { throw e; } - switch (object.type) { - case 'Image': - createImage(actor, object); - break; - - case 'Note': - case 'Question': - case 'Article': + if (validPost.includes(object.type)) { createNote(resolver, actor, object); - break; - - default: + } else { logger.warn(`Unknown type: ${object.type}`); - break; } }; diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts index 199a6a618..be7779e02 100644 --- a/src/remote/activitypub/kernel/delete/index.ts +++ b/src/remote/activitypub/kernel/delete/index.ts @@ -1,9 +1,8 @@ import Resolver from '../../resolver'; import deleteNote from './note'; import { IRemoteUser } from '../../../../models/entities/user'; -import { IDelete } from '../../type'; +import { IDelete, getApId, validPost } from '../../type'; import { apLogger } from '../../logger'; -import { Notes } from '../../../../models'; /** * 削除アクティビティを捌きます @@ -17,24 +16,11 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise => { const object = await resolver.resolve(activity.object); - const uri = (object as any).id; + const uri = getApId(object); - switch (object.type) { - case 'Note': - case 'Question': - case 'Article': - deleteNote(actor, uri); - break; - - case 'Tombstone': - const note = await Notes.findOne({ uri }); - if (note != null) { - deleteNote(actor, uri); - } - break; - - default: - apLogger.warn(`Unknown type: ${object.type}`); - break; + if (validPost.includes(object.type) || object.type === 'Tombstone') { + deleteNote(actor, uri); + } else { + apLogger.warn(`Unknown type: ${object.type}`); } }; diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index bb9465d90..14425d749 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 { Notes, Emojis, Polls } from '../../../models'; import { Note } from '../../../models/entities/note'; -import { IObject, INote } from '../type'; +import { IObject, INote, getApIds, getOneApId, getApId, validPost } from '../type'; import { Emoji } from '../../../models/entities/emoji'; import { genId } from '../../../misc/gen-id'; import { fetchMeta } from '../../../misc/fetch-meta'; @@ -32,7 +32,7 @@ export function validateNote(object: any, uri: string) { return new Error('invalid Note: object is null'); } - if (!['Note', 'Question', 'Article'].includes(object.type)) { + if (!validPost.includes(object.type)) { return new Error(`invalid Note: invalied object type ${object.type}`); } @@ -40,7 +40,7 @@ export function validateNote(object: any, uri: string) { return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${extractDbHost(object.id)}`); } - if (object.attributedTo && extractDbHost(object.attributedTo) !== expectHost) { + if (object.attributedTo && extractDbHost(getOneApId(object.attributedTo)) !== expectHost) { return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${extractDbHost(object.attributedTo)}`); } @@ -53,8 +53,7 @@ export function validateNote(object: any, uri: string) { * Misskeyに対象のNoteが登録されていればそれを返します。 */ export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise { - const uri = typeof value == 'string' ? value : value.id; - if (uri == null) throw new Error('missing uri'); + const uri = getApId(value); // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(config.url + '/')) { @@ -76,12 +75,12 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P /** * Noteを作成します。 */ -export async function createNote(value: any, resolver?: Resolver, silent = false): Promise { +export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { if (resolver == null) resolver = new Resolver(); const object: any = await resolver.resolve(value); - const entryUri = value.id || value; + const entryUri = getApId(value); const err = validateNote(object, entryUri); if (err) { logger.error(`${err.message}`, { @@ -101,7 +100,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false logger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ - const actor = await resolvePerson(note.attributedTo, resolver) as IRemoteUser; + const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser; // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { @@ -109,24 +108,24 @@ export async function createNote(value: any, resolver?: Resolver, silent = false } //#region Visibility - note.to = note.to == null ? [] : typeof note.to == 'string' ? [note.to] : note.to; - note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc; + const to = getApIds(note.to); + const cc = getApIds(note.cc); let visibility = 'public'; let visibleUsers: User[] = []; - if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) { - if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) { + if (!to.includes('https://www.w3.org/ns/activitystreams#Public')) { + if (cc.includes('https://www.w3.org/ns/activitystreams#Public')) { visibility = 'home'; - } else if (note.to.includes(`${actor.uri}/followers`)) { // TODO: person.followerと照合するべき? + } else if (to.includes(`${actor.uri}/followers`)) { // TODO: person.followerと照合するべき? visibility = 'followers'; } else { visibility = 'specified'; - visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, resolver))); + visibleUsers = await Promise.all(to.map(uri => resolvePerson(uri, resolver))); } } //#endergion - const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver); + const apMentions = await extractMentionedUsers(actor, to, cc, resolver); const apHashtags = await extractHashtags(note.tag); @@ -217,11 +216,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const apEmojis = emojis.map(emoji => emoji.name); const questionUri = note._misskey_question; - const poll = await extractPollFromQuestion(note._misskey_question || note).catch(() => undefined); + const poll = await extractPollFromQuestion(note._misskey_question || note, resolver).catch(() => undefined); // ユーザーの情報が古かったらついでに更新しておく if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { - updatePerson(note.attributedTo); + if (actor.uri) updatePerson(actor.uri); } return await post(actor, { diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts index 5c126c3a5..01086a7cf 100644 --- a/src/remote/activitypub/models/question.ts +++ b/src/remote/activitypub/models/question.ts @@ -1,12 +1,19 @@ import config from '../../../config'; import Resolver from '../resolver'; -import { IQuestion } from '../type'; +import { IObject, IQuestion, isQuestion, } from '../type'; import { apLogger } from '../logger'; import { Notes, Polls } from '../../../models'; import { IPoll } from '../../../models/entities/poll'; -export async function extractPollFromQuestion(source: string | IQuestion): Promise { - const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source; +export async function extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise { + if (resolver == null) resolver = new Resolver(); + + const question = await resolver.resolve(source); + + if (!isQuestion(question)) { + throw new Error('invalid type'); + } + const multiple = !question.oneOf; const expiresAt = question.endTime ? new Date(question.endTime) : null; diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 95c69fb8a..66163d39b 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -6,9 +6,9 @@ export interface IObject { id?: string; summary?: string; published?: string; - cc?: string[]; - to?: string[]; - attributedTo: string; + cc?: IObject | string | (IObject | string)[]; + to?: IObject | string | (IObject | string)[]; + attributedTo: IObject | string | (IObject | string)[]; attachment?: any[]; inReplyTo?: any; replies?: ICollection; @@ -23,6 +23,32 @@ export interface IObject { sensitive?: boolean; } +/** + * Get array of ActivityStreams Objects id + */ +export function getApIds(value: IObject | string | (IObject | string)[] | undefined): string[] { + if (value == null) return []; + const array = Array.isArray(value) ? value : [value]; + return array.map(x => getApId(x)); +} + +/** + * Get first ActivityStreams Object id + */ +export function getOneApId(value: IObject | string | (IObject | string)[]): string { + const firstOne = Array.isArray(value) ? value[0] : value; + return getApId(firstOne); +} + +/** + * Get ActivityStreams Object id + */ +export function getApId(value: string | IObject): string { + if (typeof value === 'string') return value; + if (typeof value.id === 'string') return value.id; + throw new Error(`cannot detemine id`); +} + export interface IActivity extends IObject { //type: 'Activity'; actor: IObject | string; @@ -42,8 +68,10 @@ export interface IOrderedCollection extends IObject { orderedItems: IObject | string | IObject[] | string[]; } +export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video']; + export interface INote extends IObject { - type: 'Note' | 'Question'; + type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video'; _misskey_content?: string; _misskey_quote?: string; _misskey_question?: string; @@ -59,6 +87,9 @@ export interface IQuestion extends IObject { endTime?: Date; } +export const isQuestion = (object: IObject): object is IQuestion => + object.type === 'Note' || object.type === 'Question'; + interface IQuestionChoice { name?: string; replies?: ICollection; diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts index 9724a044b..bbaa1fa10 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 } from '../../../../remote/activitypub/type'; +import { validActor, validPost } from '../../../../remote/activitypub/type'; export const meta = { tags: ['federation'], @@ -145,7 +145,7 @@ async function fetchAny(uri: string) { }; } - if (['Note', 'Question', 'Article'].includes(object.type)) { + if (validPost.includes(object.type)) { const note = await createNote(object.id, undefined, true); return { type: 'Note',