From 08beb45935961066ddc543b1659f5cc6e891aa6c Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 9 Apr 2018 04:08:56 +0900 Subject: wip --- src/queue/processors/http/process-inbox.ts | 6 +- src/remote/activitypub/act/announce/index.ts | 39 ------- src/remote/activitypub/act/announce/note.ts | 52 --------- src/remote/activitypub/act/create/image.ts | 18 --- src/remote/activitypub/act/create/index.ts | 44 ------- src/remote/activitypub/act/create/note.ts | 87 -------------- src/remote/activitypub/act/delete/index.ts | 36 ------ src/remote/activitypub/act/delete/note.ts | 30 ----- src/remote/activitypub/act/follow.ts | 24 ---- src/remote/activitypub/act/index.ts | 51 -------- src/remote/activitypub/act/like.ts | 20 ---- src/remote/activitypub/act/undo/follow.ts | 24 ---- src/remote/activitypub/act/undo/index.ts | 37 ------ src/remote/activitypub/objects/image.ts | 29 +++++ src/remote/activitypub/objects/note.ts | 110 ++++++++++++++++++ src/remote/activitypub/objects/person.ts | 142 +++++++++++++++++++++++ src/remote/activitypub/perform/announce/index.ts | 39 +++++++ src/remote/activitypub/perform/announce/note.ts | 45 +++++++ src/remote/activitypub/perform/create/image.ts | 6 + src/remote/activitypub/perform/create/index.ts | 44 +++++++ src/remote/activitypub/perform/create/note.ts | 13 +++ src/remote/activitypub/perform/delete/index.ts | 36 ++++++ src/remote/activitypub/perform/delete/note.ts | 30 +++++ src/remote/activitypub/perform/follow.ts | 24 ++++ src/remote/activitypub/perform/index.ts | 51 ++++++++ src/remote/activitypub/perform/like.ts | 20 ++++ src/remote/activitypub/perform/undo/follow.ts | 24 ++++ src/remote/activitypub/perform/undo/index.ts | 37 ++++++ src/remote/activitypub/resolve-person.ts | 98 ---------------- src/remote/activitypub/type.ts | 16 +++ src/remote/resolve-user.ts | 6 +- src/remote/webfinger.ts | 27 +---- 32 files changed, 678 insertions(+), 587 deletions(-) delete mode 100644 src/remote/activitypub/act/announce/index.ts delete mode 100644 src/remote/activitypub/act/announce/note.ts delete mode 100644 src/remote/activitypub/act/create/image.ts delete mode 100644 src/remote/activitypub/act/create/index.ts delete mode 100644 src/remote/activitypub/act/create/note.ts delete mode 100644 src/remote/activitypub/act/delete/index.ts delete mode 100644 src/remote/activitypub/act/delete/note.ts delete mode 100644 src/remote/activitypub/act/follow.ts delete mode 100644 src/remote/activitypub/act/index.ts delete mode 100644 src/remote/activitypub/act/like.ts delete mode 100644 src/remote/activitypub/act/undo/follow.ts delete mode 100644 src/remote/activitypub/act/undo/index.ts create mode 100644 src/remote/activitypub/objects/image.ts create mode 100644 src/remote/activitypub/objects/note.ts create mode 100644 src/remote/activitypub/objects/person.ts create mode 100644 src/remote/activitypub/perform/announce/index.ts create mode 100644 src/remote/activitypub/perform/announce/note.ts create mode 100644 src/remote/activitypub/perform/create/image.ts create mode 100644 src/remote/activitypub/perform/create/index.ts create mode 100644 src/remote/activitypub/perform/create/note.ts create mode 100644 src/remote/activitypub/perform/delete/index.ts create mode 100644 src/remote/activitypub/perform/delete/note.ts create mode 100644 src/remote/activitypub/perform/follow.ts create mode 100644 src/remote/activitypub/perform/index.ts create mode 100644 src/remote/activitypub/perform/like.ts create mode 100644 src/remote/activitypub/perform/undo/follow.ts create mode 100644 src/remote/activitypub/perform/undo/index.ts delete mode 100644 src/remote/activitypub/resolve-person.ts (limited to 'src') diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index 6608907a7a..ce5b7d5a89 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -4,8 +4,8 @@ import * as debug from 'debug'; import { verifySignature } from 'http-signature'; import parseAcct from '../../../acct/parse'; import User, { IRemoteUser } from '../../../models/user'; -import act from '../../../remote/activitypub/act'; -import resolvePerson from '../../../remote/activitypub/resolve-person'; +import perform from '../../../remote/activitypub/perform'; +import { resolvePerson } from '../../../remote/activitypub/objects/person'; const log = debug('misskey:queue:inbox'); @@ -58,7 +58,7 @@ export default async (job: kue.Job, done): Promise => { // アクティビティを処理 try { - await act(user, activity); + await perform(user, activity); done(); } catch (e) { done(e); diff --git a/src/remote/activitypub/act/announce/index.ts b/src/remote/activitypub/act/announce/index.ts deleted file mode 100644 index c3ac06607d..0000000000 --- a/src/remote/activitypub/act/announce/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; -import announceNote from './note'; -import { IAnnounce } from '../../type'; - -const log = debug('misskey:activitypub'); - -export default async (actor: IRemoteUser, activity: IAnnounce): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - log(`Announce: ${uri}`); - - const resolver = new Resolver(); - - let object; - - try { - object = await resolver.resolve(activity.object); - } catch (e) { - log(`Resolution failed: ${e}`); - throw e; - } - - switch (object.type) { - case 'Note': - announceNote(resolver, actor, activity, object); - break; - - default: - console.warn(`Unknown announce type: ${object.type}`); - break; - } -}; diff --git a/src/remote/activitypub/act/announce/note.ts b/src/remote/activitypub/act/announce/note.ts deleted file mode 100644 index 24d159f184..0000000000 --- a/src/remote/activitypub/act/announce/note.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import Note from '../../../../models/note'; -import post from '../../../../services/note/create'; -import { IRemoteUser, isRemoteUser } from '../../../../models/user'; -import { IAnnounce, INote } from '../../type'; -import createNote from '../create/note'; -import resolvePerson from '../../resolve-person'; - -const log = debug('misskey:activitypub'); - -/** - * アナウンスアクティビティを捌きます - */ -export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise { - const uri = activity.id || activity; - - if (typeof uri !== 'string') { - throw new Error('invalid announce'); - } - - // 既に同じURIを持つものが登録されていないかチェック - const exist = await Note.findOne({ uri }); - if (exist) { - return; - } - - // アナウンス元の投稿の投稿者をフェッチ - const announcee = await resolvePerson(note.attributedTo); - - const renote = isRemoteUser(announcee) - ? await createNote(resolver, announcee, note, true) - : await Note.findOne({ _id: note.id.split('/').pop() }); - - log(`Creating the (Re)Note: ${uri}`); - - //#region Visibility - let visibility = 'public'; - if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; - if (activity.cc.length == 0) visibility = 'private'; - // TODO - if (visibility != 'public') throw new Error('unspported visibility'); - //#endergion - - await post(actor, { - createdAt: new Date(activity.published), - renote, - visibility, - uri - }); -} diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts deleted file mode 100644 index f1462f4ee4..0000000000 --- a/src/remote/activitypub/act/create/image.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as debug from 'debug'; - -import uploadFromUrl from '../../../../services/drive/upload-from-url'; -import { IRemoteUser } from '../../../../models/user'; -import { IDriveFile } from '../../../../models/drive-file'; - -const log = debug('misskey:activitypub'); - -export default async function(actor: IRemoteUser, image): Promise { - if ('attributedTo' in image && actor.uri !== image.attributedTo) { - log(`invalid image: ${JSON.stringify(image, null, 2)}`); - throw new Error('invalid image'); - } - - log(`Creating the Image: ${image.url}`); - - return await uploadFromUrl(image.url, actor); -} diff --git a/src/remote/activitypub/act/create/index.ts b/src/remote/activitypub/act/create/index.ts deleted file mode 100644 index 7cb9b08449..0000000000 --- a/src/remote/activitypub/act/create/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; -import createNote from './note'; -import createImage from './image'; -import { ICreate } from '../../type'; - -const log = debug('misskey:activitypub'); - -export default async (actor: IRemoteUser, activity: ICreate): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - log(`Create: ${uri}`); - - const resolver = new Resolver(); - - let object; - - try { - object = await resolver.resolve(activity.object); - } catch (e) { - log(`Resolution failed: ${e}`); - throw e; - } - - switch (object.type) { - case 'Image': - createImage(actor, object); - break; - - case 'Note': - createNote(resolver, actor, object); - break; - - default: - console.warn(`Unknown type: ${object.type}`); - break; - } -}; diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts deleted file mode 100644 index 599bc10aa8..0000000000 --- a/src/remote/activitypub/act/create/note.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { JSDOM } from 'jsdom'; -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import Note, { INote } from '../../../../models/note'; -import post from '../../../../services/note/create'; -import { IRemoteUser } from '../../../../models/user'; -import resolvePerson from '../../resolve-person'; -import createImage from './image'; -import config from '../../../../config'; - -const log = debug('misskey:activitypub'); - -/** - * 投稿作成アクティビティを捌きます - */ -export default async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise { - if (typeof note.id !== 'string') { - log(`invalid note: ${JSON.stringify(note, null, 2)}`); - throw new Error('invalid note'); - } - - // 既に同じURIを持つものが登録されていないかチェックし、登録されていたらそれを返す - const exist = await Note.findOne({ uri: note.id }); - if (exist) { - return exist; - } - - log(`Creating the Note: ${note.id}`); - - //#region Visibility - let visibility = 'public'; - if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; - if (note.cc.length == 0) visibility = 'private'; - // TODO - if (visibility != 'public') throw new Error('unspported visibility'); - //#endergion - - //#region 添付メディア - let media = []; - if ('attachment' in note && note.attachment != null) { - // TODO: attachmentは必ずしもImageではない - // TODO: attachmentは必ずしも配列ではない - media = await Promise.all(note.attachment.map(x => { - return createImage(actor, x); - })); - } - //#endregion - - //#region リプライ - let reply = null; - if ('inReplyTo' in note && note.inReplyTo != null) { - // リプライ先の投稿がMisskeyに登録されているか調べる - const uri: string = note.inReplyTo.id || note.inReplyTo; - const inReplyToNote = uri.startsWith(config.url + '/') - ? await Note.findOne({ _id: uri.split('/').pop() }) - : await Note.findOne({ uri }); - - if (inReplyToNote) { - reply = inReplyToNote; - } else { - // 無かったらフェッチ - const inReplyTo = await resolver.resolve(note.inReplyTo) as any; - - // リプライ先の投稿の投稿者をフェッチ - const actor = await resolvePerson(inReplyTo.attributedTo) as IRemoteUser; - - // TODO: silentを常にtrueにしてはならない - reply = await createNote(resolver, actor, inReplyTo); - } - } - //#endregion - - const { window } = new JSDOM(note.content); - - return await post(actor, { - createdAt: new Date(note.published), - media, - reply, - renote: undefined, - text: window.document.body.textContent, - viaMobile: false, - geo: undefined, - visibility, - uri: note.id - }); -} diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts deleted file mode 100644 index 10b47dc4ca..0000000000 --- a/src/remote/activitypub/act/delete/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Resolver from '../../resolver'; -import deleteNote from './note'; -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; - -/** - * 削除アクティビティを捌きます - */ -export default async (actor: IRemoteUser, activity): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object); - - const uri = (object as any).id; - - switch (object.type) { - case 'Note': - deleteNote(actor, uri); - break; - - case 'Tombstone': - const note = await Note.findOne({ uri }); - if (note != null) { - deleteNote(actor, uri); - } - break; - - default: - console.warn(`Unknown type: ${object.type}`); - break; - } -}; diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/act/delete/note.ts deleted file mode 100644 index 64c342d39b..0000000000 --- a/src/remote/activitypub/act/delete/note.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as debug from 'debug'; - -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; - -const log = debug('misskey:activitypub'); - -export default async function(actor: IRemoteUser, uri: string): Promise { - log(`Deleting the Note: ${uri}`); - - const note = await Note.findOne({ uri }); - - if (note == null) { - throw new Error('note not found'); - } - - if (!note.userId.equals(actor._id)) { - throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); - } - - Note.update({ _id: note._id }, { - $set: { - deletedAt: new Date(), - text: null, - textHtml: null, - mediaIds: [], - poll: null - } - }); -} diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts deleted file mode 100644 index 6a8b5a1bec..0000000000 --- a/src/remote/activitypub/act/follow.ts +++ /dev/null @@ -1,24 +0,0 @@ -import User, { IRemoteUser } from '../../../models/user'; -import config from '../../../config'; -import follow from '../../../services/following/create'; -import { IFollow } from '../type'; - -export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - - if (!id.startsWith(config.url + '/')) { - return null; - } - - const followee = await User.findOne({ _id: id.split('/').pop() }); - - if (followee === null) { - throw new Error('followee not found'); - } - - if (followee.host != null) { - throw new Error('フォローしようとしているユーザーはローカルユーザーではありません'); - } - - await follow(actor, followee, activity); -}; diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts deleted file mode 100644 index 15ea9494ae..0000000000 --- a/src/remote/activitypub/act/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Object } from '../type'; -import { IRemoteUser } from '../../../models/user'; -import create from './create'; -import performDeleteActivity from './delete'; -import follow from './follow'; -import undo from './undo'; -import like from './like'; -import announce from './announce'; - -const self = async (actor: IRemoteUser, activity: Object): Promise => { - switch (activity.type) { - case 'Create': - await create(actor, activity); - break; - - case 'Delete': - await performDeleteActivity(actor, activity); - break; - - case 'Follow': - await follow(actor, activity); - break; - - case 'Accept': - // noop - break; - - case 'Announce': - await announce(actor, activity); - break; - - case 'Like': - await like(actor, activity); - break; - - case 'Undo': - await undo(actor, activity); - break; - - case 'Collection': - case 'OrderedCollection': - // TODO - break; - - default: - console.warn(`unknown activity type: ${(activity as any).type}`); - return null; - } -}; - -export default self; diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/act/like.ts deleted file mode 100644 index 4941608588..0000000000 --- a/src/remote/activitypub/act/like.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Note from '../../../models/note'; -import { IRemoteUser } from '../../../models/user'; -import { ILike } from '../type'; -import create from '../../../services/note/reaction/create'; - -export default async (actor: IRemoteUser, activity: ILike) => { - const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - - // Transform: - // https://misskey.ex/notes/xxxx to - // xxxx - const noteId = id.split('/').pop(); - - const note = await Note.findOne({ _id: noteId }); - if (note === null) { - throw new Error(); - } - - await create(actor, note, 'pudding'); -}; diff --git a/src/remote/activitypub/act/undo/follow.ts b/src/remote/activitypub/act/undo/follow.ts deleted file mode 100644 index a85cb0305d..0000000000 --- a/src/remote/activitypub/act/undo/follow.ts +++ /dev/null @@ -1,24 +0,0 @@ -import User, { IRemoteUser } from '../../../../models/user'; -import config from '../../../../config'; -import unfollow from '../../../../services/following/delete'; -import { IFollow } from '../../type'; - -export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - - if (!id.startsWith(config.url + '/')) { - return null; - } - - const followee = await User.findOne({ _id: id.split('/').pop() }); - - if (followee === null) { - throw new Error('followee not found'); - } - - if (followee.host != null) { - throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); - } - - await unfollow(actor, followee, activity); -}; diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts deleted file mode 100644 index 71f547aeb9..0000000000 --- a/src/remote/activitypub/act/undo/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as debug from 'debug'; - -import { IRemoteUser } from '../../../../models/user'; -import { IUndo } from '../../type'; -import unfollow from './follow'; -import Resolver from '../../resolver'; - -const log = debug('misskey:activitypub'); - -export default async (actor: IRemoteUser, activity: IUndo): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - log(`Undo: ${uri}`); - - const resolver = new Resolver(); - - let object; - - try { - object = await resolver.resolve(activity.object); - } catch (e) { - log(`Resolution failed: ${e}`); - throw e; - } - - switch (object.type) { - case 'Follow': - unfollow(actor, object); - break; - } - - return null; -}; diff --git a/src/remote/activitypub/objects/image.ts b/src/remote/activitypub/objects/image.ts new file mode 100644 index 0000000000..7f79fc5c06 --- /dev/null +++ b/src/remote/activitypub/objects/image.ts @@ -0,0 +1,29 @@ +import * as debug from 'debug'; + +import uploadFromUrl from '../../../services/drive/upload-from-url'; +import { IRemoteUser } from '../../../models/user'; +import { IDriveFile } from '../../../models/drive-file'; + +const log = debug('misskey:activitypub'); + +/** + * Imageを作成します。 + */ +export async function createImage(actor: IRemoteUser, image): Promise { + log(`Creating the Image: ${image.url}`); + + return await uploadFromUrl(image.url, actor); +} + +/** + * Imageを解決します。 + * + * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolveImage(actor: IRemoteUser, value: any): Promise { + // TODO + + // リモートサーバーからフェッチしてきて登録 + return await createImage(actor, value); +} diff --git a/src/remote/activitypub/objects/note.ts b/src/remote/activitypub/objects/note.ts new file mode 100644 index 0000000000..3edcb8c63f --- /dev/null +++ b/src/remote/activitypub/objects/note.ts @@ -0,0 +1,110 @@ +import { JSDOM } from 'jsdom'; +import * as debug from 'debug'; + +import config from '../../../config'; +import Resolver from '../resolver'; +import Note, { INote } from '../../../models/note'; +import post from '../../../services/note/create'; +import { INote as INoteActivityStreamsObject, IObject } from '../type'; +import { resolvePerson } from './person'; +import { resolveImage } from './image'; +import { IRemoteUser } from '../../../models/user'; + +const log = debug('misskey:activitypub'); + +/** + * Noteをフェッチします。 + * + * Misskeyに対象のNoteが登録されていればそれを返します。 + */ +export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise { + const uri = typeof value == 'string' ? value : value.id; + + // URIがこのサーバーを指しているならデータベースからフェッチ + if (uri.startsWith(config.url + '/')) { + return await Note.findOne({ _id: uri.split('/').pop() }); + } + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await Note.findOne({ uri }); + + if (exist) { + return exist; + } + //#endregion + + return null; +} + +/** + * Noteを作成します。 + */ +export async function createNote(value: any, resolver?: Resolver, silent = false): Promise { + if (resolver == null) resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; + + if (object == null || object.type !== 'Note') { + throw new Error('invalid note'); + } + + const note: INoteActivityStreamsObject = object; + + log(`Creating the Note: ${note.id}`); + + // 投稿者をフェッチ + const actor = await resolvePerson(note.attributedTo) as IRemoteUser; + + //#region Visibility + let visibility = 'public'; + if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; + if (note.cc.length == 0) visibility = 'private'; + // TODO + if (visibility != 'public') throw new Error('unspported visibility'); + //#endergion + + // 添付メディア + // TODO: attachmentは必ずしもImageではない + // TODO: attachmentは必ずしも配列ではない + const media = note.attachment + ? await Promise.all(note.attachment.map(x => resolveImage(actor, x))) + : []; + + // リプライ + const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null; + + const { window } = new JSDOM(note.content); + + return await post(actor, { + createdAt: new Date(note.published), + media, + reply, + renote: undefined, + text: window.document.body.textContent, + viaMobile: false, + geo: undefined, + visibility, + uri: note.id + }, silent); +} + +/** + * Noteを解決します。 + * + * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise { + const uri = typeof value == 'string' ? value : value.id; + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await fetchNote(uri); + + if (exist) { + return exist; + } + //#endregion + + // リモートサーバーからフェッチしてきて登録 + return await createNote(value, resolver); +} diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts new file mode 100644 index 0000000000..b1e8c9ee0a --- /dev/null +++ b/src/remote/activitypub/objects/person.ts @@ -0,0 +1,142 @@ +import { JSDOM } from 'jsdom'; +import { toUnicode } from 'punycode'; +import * as debug from 'debug'; + +import config from '../../../config'; +import User, { validateUsername, isValidName, isValidDescription, IUser, IRemoteUser } from '../../../models/user'; +import webFinger from '../../webfinger'; +import Resolver from '../resolver'; +import { resolveImage } from './image'; +import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type'; + +const log = debug('misskey:activitypub'); + +/** + * Personをフェッチします。 + * + * Misskeyに対象のPersonが登録されていればそれを返します。 + */ +export async function fetchPerson(value: string | IObject, resolver?: Resolver): Promise { + const uri = typeof value == 'string' ? value : value.id; + + // URIがこのサーバーを指しているならデータベースからフェッチ + if (uri.startsWith(config.url + '/')) { + return await User.findOne({ _id: uri.split('/').pop() }); + } + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await User.findOne({ uri }); + + if (exist) { + return exist; + } + //#endregion + + return null; +} + +/** + * Personを作成します。 + */ +export async function createPerson(value: any, resolver?: Resolver): Promise { + if (resolver == null) resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; + + if ( + object == null || + object.type !== 'Person' || + typeof object.preferredUsername !== 'string' || + !validateUsername(object.preferredUsername) || + !isValidName(object.name == '' ? null : object.name) || + !isValidDescription(object.summary) + ) { + throw new Error('invalid person'); + } + + const person: IPerson = object; + + log(`Creating the Person: ${person.id}`); + + const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([ + resolver.resolve(person.followers).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined + ), + resolver.resolve(person.following).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined + ), + resolver.resolve(person.outbox).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined + ), + webFinger(person.id) + ]); + + const host = toUnicode(finger.subject.replace(/^.*?@/, '')); + const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase()); + const summaryDOM = JSDOM.fragment(person.summary); + + // Create user + const user = await User.insert({ + avatarId: null, + bannerId: null, + createdAt: Date.parse(person.published) || null, + description: summaryDOM.textContent, + followersCount, + followingCount, + notesCount, + name: person.name, + driveCapacity: 1024 * 1024 * 8, // 8MiB + username: person.preferredUsername, + usernameLower: person.preferredUsername.toLowerCase(), + host, + hostLower, + publicKey: { + id: person.publicKey.id, + publicKeyPem: person.publicKey.publicKeyPem + }, + inbox: person.inbox, + uri: person.id + }) as IRemoteUser; + + //#region アイコンとヘッダー画像をフェッチ + const [avatarId, bannerId] = (await Promise.all([ + person.icon, + person.image + ].map(img => + img == null + ? Promise.resolve(null) + : resolveImage(user, img.url) + ))).map(file => file != null ? file._id : null); + + User.update({ _id: user._id }, { $set: { avatarId, bannerId } }); + + user.avatarId = avatarId; + user.bannerId = bannerId; + //#endregion + + return user; +} + +/** + * Personを解決します。 + * + * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolvePerson(value: string | IObject, verifier?: string): Promise { + const uri = typeof value == 'string' ? value : value.id; + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await fetchPerson(uri); + + if (exist) { + return exist; + } + //#endregion + + // リモートサーバーからフェッチしてきて登録 + return await createPerson(value); +} diff --git a/src/remote/activitypub/perform/announce/index.ts b/src/remote/activitypub/perform/announce/index.ts new file mode 100644 index 0000000000..c3ac06607d --- /dev/null +++ b/src/remote/activitypub/perform/announce/index.ts @@ -0,0 +1,39 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import announceNote from './note'; +import { IAnnounce } from '../../type'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IAnnounce): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Announce: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Note': + announceNote(resolver, actor, activity, object); + break; + + default: + console.warn(`Unknown announce type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/perform/announce/note.ts b/src/remote/activitypub/perform/announce/note.ts new file mode 100644 index 0000000000..68fb23c97f --- /dev/null +++ b/src/remote/activitypub/perform/announce/note.ts @@ -0,0 +1,45 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import post from '../../../../services/note/create'; +import { IRemoteUser } from '../../../../models/user'; +import { IAnnounce, INote } from '../../type'; +import { fetchNote, resolveNote } from '../../objects/note'; + +const log = debug('misskey:activitypub'); + +/** + * アナウンスアクティビティを捌きます + */ +export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise { + const uri = activity.id || activity; + + if (typeof uri !== 'string') { + throw new Error('invalid announce'); + } + + // 既に同じURIを持つものが登録されていないかチェック + const exist = await fetchNote(uri); + if (exist) { + return; + } + + const renote = await resolveNote(note); + + log(`Creating the (Re)Note: ${uri}`); + + //#region Visibility + let visibility = 'public'; + if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; + if (activity.cc.length == 0) visibility = 'private'; + // TODO + if (visibility != 'public') throw new Error('unspported visibility'); + //#endergion + + await post(actor, { + createdAt: new Date(activity.published), + renote, + visibility, + uri + }); +} diff --git a/src/remote/activitypub/perform/create/image.ts b/src/remote/activitypub/perform/create/image.ts new file mode 100644 index 0000000000..ea36545f0c --- /dev/null +++ b/src/remote/activitypub/perform/create/image.ts @@ -0,0 +1,6 @@ +import { IRemoteUser } from '../../../../models/user'; +import { createImage } from '../../objects/image'; + +export default async function(actor: IRemoteUser, image): Promise { + await createImage(image.url, actor); +} diff --git a/src/remote/activitypub/perform/create/index.ts b/src/remote/activitypub/perform/create/index.ts new file mode 100644 index 0000000000..7cb9b08449 --- /dev/null +++ b/src/remote/activitypub/perform/create/index.ts @@ -0,0 +1,44 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import createNote from './note'; +import createImage from './image'; +import { ICreate } from '../../type'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: ICreate): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Create: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Image': + createImage(actor, object); + break; + + case 'Note': + createNote(resolver, actor, object); + break; + + default: + console.warn(`Unknown type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/perform/create/note.ts b/src/remote/activitypub/perform/create/note.ts new file mode 100644 index 0000000000..530cf6483f --- /dev/null +++ b/src/remote/activitypub/perform/create/note.ts @@ -0,0 +1,13 @@ +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import { createNote, fetchNote } from '../../objects/note'; + +/** + * 投稿作成アクティビティを捌きます + */ +export default async function(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise { + const exist = await fetchNote(note); + if (exist == null) { + await createNote(note); + } +} diff --git a/src/remote/activitypub/perform/delete/index.ts b/src/remote/activitypub/perform/delete/index.ts new file mode 100644 index 0000000000..10b47dc4ca --- /dev/null +++ b/src/remote/activitypub/perform/delete/index.ts @@ -0,0 +1,36 @@ +import Resolver from '../../resolver'; +import deleteNote from './note'; +import Note from '../../../../models/note'; +import { IRemoteUser } from '../../../../models/user'; + +/** + * 削除アクティビティを捌きます + */ +export default async (actor: IRemoteUser, activity): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const resolver = new Resolver(); + + const object = await resolver.resolve(activity.object); + + const uri = (object as any).id; + + switch (object.type) { + case 'Note': + deleteNote(actor, uri); + break; + + case 'Tombstone': + const note = await Note.findOne({ uri }); + if (note != null) { + deleteNote(actor, uri); + } + break; + + default: + console.warn(`Unknown type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/perform/delete/note.ts b/src/remote/activitypub/perform/delete/note.ts new file mode 100644 index 0000000000..64c342d39b --- /dev/null +++ b/src/remote/activitypub/perform/delete/note.ts @@ -0,0 +1,30 @@ +import * as debug from 'debug'; + +import Note from '../../../../models/note'; +import { IRemoteUser } from '../../../../models/user'; + +const log = debug('misskey:activitypub'); + +export default async function(actor: IRemoteUser, uri: string): Promise { + log(`Deleting the Note: ${uri}`); + + const note = await Note.findOne({ uri }); + + if (note == null) { + throw new Error('note not found'); + } + + if (!note.userId.equals(actor._id)) { + throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); + } + + Note.update({ _id: note._id }, { + $set: { + deletedAt: new Date(), + text: null, + textHtml: null, + mediaIds: [], + poll: null + } + }); +} diff --git a/src/remote/activitypub/perform/follow.ts b/src/remote/activitypub/perform/follow.ts new file mode 100644 index 0000000000..6a8b5a1bec --- /dev/null +++ b/src/remote/activitypub/perform/follow.ts @@ -0,0 +1,24 @@ +import User, { IRemoteUser } from '../../../models/user'; +import config from '../../../config'; +import follow from '../../../services/following/create'; +import { IFollow } from '../type'; + +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + if (!id.startsWith(config.url + '/')) { + return null; + } + + const followee = await User.findOne({ _id: id.split('/').pop() }); + + if (followee === null) { + throw new Error('followee not found'); + } + + if (followee.host != null) { + throw new Error('フォローしようとしているユーザーはローカルユーザーではありません'); + } + + await follow(actor, followee, activity); +}; diff --git a/src/remote/activitypub/perform/index.ts b/src/remote/activitypub/perform/index.ts new file mode 100644 index 0000000000..15ea9494ae --- /dev/null +++ b/src/remote/activitypub/perform/index.ts @@ -0,0 +1,51 @@ +import { Object } from '../type'; +import { IRemoteUser } from '../../../models/user'; +import create from './create'; +import performDeleteActivity from './delete'; +import follow from './follow'; +import undo from './undo'; +import like from './like'; +import announce from './announce'; + +const self = async (actor: IRemoteUser, activity: Object): Promise => { + switch (activity.type) { + case 'Create': + await create(actor, activity); + break; + + case 'Delete': + await performDeleteActivity(actor, activity); + break; + + case 'Follow': + await follow(actor, activity); + break; + + case 'Accept': + // noop + break; + + case 'Announce': + await announce(actor, activity); + break; + + case 'Like': + await like(actor, activity); + break; + + case 'Undo': + await undo(actor, activity); + break; + + case 'Collection': + case 'OrderedCollection': + // TODO + break; + + default: + console.warn(`unknown activity type: ${(activity as any).type}`); + return null; + } +}; + +export default self; diff --git a/src/remote/activitypub/perform/like.ts b/src/remote/activitypub/perform/like.ts new file mode 100644 index 0000000000..4941608588 --- /dev/null +++ b/src/remote/activitypub/perform/like.ts @@ -0,0 +1,20 @@ +import Note from '../../../models/note'; +import { IRemoteUser } from '../../../models/user'; +import { ILike } from '../type'; +import create from '../../../services/note/reaction/create'; + +export default async (actor: IRemoteUser, activity: ILike) => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + // Transform: + // https://misskey.ex/notes/xxxx to + // xxxx + const noteId = id.split('/').pop(); + + const note = await Note.findOne({ _id: noteId }); + if (note === null) { + throw new Error(); + } + + await create(actor, note, 'pudding'); +}; diff --git a/src/remote/activitypub/perform/undo/follow.ts b/src/remote/activitypub/perform/undo/follow.ts new file mode 100644 index 0000000000..a85cb0305d --- /dev/null +++ b/src/remote/activitypub/perform/undo/follow.ts @@ -0,0 +1,24 @@ +import User, { IRemoteUser } from '../../../../models/user'; +import config from '../../../../config'; +import unfollow from '../../../../services/following/delete'; +import { IFollow } from '../../type'; + +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + if (!id.startsWith(config.url + '/')) { + return null; + } + + const followee = await User.findOne({ _id: id.split('/').pop() }); + + if (followee === null) { + throw new Error('followee not found'); + } + + if (followee.host != null) { + throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); + } + + await unfollow(actor, followee, activity); +}; diff --git a/src/remote/activitypub/perform/undo/index.ts b/src/remote/activitypub/perform/undo/index.ts new file mode 100644 index 0000000000..71f547aeb9 --- /dev/null +++ b/src/remote/activitypub/perform/undo/index.ts @@ -0,0 +1,37 @@ +import * as debug from 'debug'; + +import { IRemoteUser } from '../../../../models/user'; +import { IUndo } from '../../type'; +import unfollow from './follow'; +import Resolver from '../../resolver'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IUndo): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Undo: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Follow': + unfollow(actor, object); + break; + } + + return null; +}; diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts deleted file mode 100644 index 50e7873cbd..0000000000 --- a/src/remote/activitypub/resolve-person.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { JSDOM } from 'jsdom'; -import { toUnicode } from 'punycode'; -import config from '../../config'; -import User, { validateUsername, isValidName, isValidDescription, IUser } from '../../models/user'; -import webFinger from '../webfinger'; -import Resolver from './resolver'; -import uploadFromUrl from '../../services/drive/upload-from-url'; -import { isCollectionOrOrderedCollection, IObject } from './type'; - -export default async (value: string | IObject, verifier?: string): Promise => { - const id = typeof value == 'string' ? value : value.id; - - if (id.startsWith(config.url + '/')) { - return await User.findOne({ _id: id.split('/').pop() }); - } else { - const exist = await User.findOne({ - uri: id - }); - - if (exist) { - return exist; - } - } - - const resolver = new Resolver(); - - const object = await resolver.resolve(value) as any; - - if ( - object == null || - object.type !== 'Person' || - typeof object.preferredUsername !== 'string' || - !validateUsername(object.preferredUsername) || - !isValidName(object.name == '' ? null : object.name) || - !isValidDescription(object.summary) - ) { - throw new Error('invalid person'); - } - - const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([ - resolver.resolve(object.followers).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(object.following).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(object.outbox).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - webFinger(id, verifier) - ]); - - const host = toUnicode(finger.subject.replace(/^.*?@/, '')); - const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase()); - const summaryDOM = JSDOM.fragment(object.summary); - - // Create user - const user = await User.insert({ - avatarId: null, - bannerId: null, - createdAt: Date.parse(object.published) || null, - description: summaryDOM.textContent, - followersCount, - followingCount, - notesCount, - name: object.name, - driveCapacity: 1024 * 1024 * 8, // 8MiB - username: object.preferredUsername, - usernameLower: object.preferredUsername.toLowerCase(), - host, - hostLower, - publicKey: { - id: object.publicKey.id, - publicKeyPem: object.publicKey.publicKeyPem - }, - inbox: object.inbox, - uri: id - }); - - const [avatarId, bannerId] = (await Promise.all([ - object.icon, - object.image - ].map(img => - img == null - ? Promise.resolve(null) - : uploadFromUrl(img.url, user) - ))).map(file => file != null ? file._id : null); - - User.update({ _id: user._id }, { $set: { avatarId, bannerId } }); - - user.avatarId = avatarId; - user.bannerId = bannerId; - - return user; -}; diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 2335517645..983eb621fa 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -9,6 +9,11 @@ export interface IObject { cc?: string[]; to?: string[]; attributedTo: string; + attachment?: any[]; + inReplyTo?: any; + content: string; + icon?: any; + image?: any; } export interface IActivity extends IObject { @@ -34,6 +39,17 @@ export interface INote extends IObject { type: 'Note'; } +export interface IPerson extends IObject { + type: 'Person'; + name: string; + preferredUsername: string; + inbox: string; + publicKey: any; + followers: any; + following: any; + outbox: any; +} + export const isCollection = (object: IObject): object is ICollection => object.type === 'Collection'; diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts index 0e7edd8e12..346e134c9f 100644 --- a/src/remote/resolve-user.ts +++ b/src/remote/resolve-user.ts @@ -1,8 +1,8 @@ import { toUnicode, toASCII } from 'punycode'; import User from '../models/user'; -import resolvePerson from './activitypub/resolve-person'; import webFinger from './webfinger'; import config from '../config'; +import { createPerson } from './activitypub/objects/person'; export default async (username, host, option) => { const usernameLower = username.toLowerCase(); @@ -18,13 +18,13 @@ export default async (username, host, option) => { if (user === null) { const acctLower = `${usernameLower}@${hostLowerAscii}`; - const finger = await webFinger(acctLower, acctLower); + const finger = await webFinger(acctLower); const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); if (!self) { throw new Error('self link not found'); } - user = await resolvePerson(self.href, acctLower); + user = await createPerson(self.href); } return user; diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts index bfca8d1c86..4f1ff231c0 100644 --- a/src/remote/webfinger.ts +++ b/src/remote/webfinger.ts @@ -3,36 +3,21 @@ const WebFinger = require('webfinger.js'); const webFinger = new WebFinger({ }); type ILink = { - href: string; - rel: string; + href: string; + rel: string; }; type IWebFinger = { - links: ILink[]; - subject: string; + links: ILink[]; + subject: string; }; -export default async function resolve(query, verifier?: string): Promise { - const finger = await new Promise((res, rej) => webFinger.lookup(query, (error, result) => { +export default async function resolve(query): Promise { + return await new Promise((res, rej) => webFinger.lookup(query, (error, result) => { if (error) { return rej(error); } res(result.object); })) as IWebFinger; - const subject = finger.subject.toLowerCase().replace(/^acct:/, ''); - - if (typeof verifier === 'string') { - if (subject !== verifier) { - throw new Error(); - } - - return finger; - } - - if (typeof subject === 'string') { - return resolve(subject, subject); - } - - throw new Error(); } -- cgit v1.2.3-freya