summaryrefslogtreecommitdiff
path: root/src/remote/activitypub
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2018-04-11 20:27:09 +0900
committerGitHub <noreply@github.com>2018-04-11 20:27:09 +0900
commitd43fe853c3605696e2e57e240845d0fc9c284f61 (patch)
tree838914e262c0fca5737588a7bba64e2b9f3d8e5f /src/remote/activitypub
parentUpdate README.md (diff)
parentwip #1443 (diff)
downloadmisskey-d43fe853c3605696e2e57e240845d0fc9c284f61.tar.gz
misskey-d43fe853c3605696e2e57e240845d0fc9c284f61.tar.bz2
misskey-d43fe853c3605696e2e57e240845d0fc9c284f61.zip
Merge pull request #1 from syuilo/master
追従
Diffstat (limited to 'src/remote/activitypub')
-rw-r--r--src/remote/activitypub/kernel/announce/index.ts35
-rw-r--r--src/remote/activitypub/kernel/announce/note.ts45
-rw-r--r--src/remote/activitypub/kernel/create/image.ts6
-rw-r--r--src/remote/activitypub/kernel/create/index.ts40
-rw-r--r--src/remote/activitypub/kernel/create/note.ts13
-rw-r--r--src/remote/activitypub/kernel/delete/index.ts36
-rw-r--r--src/remote/activitypub/kernel/delete/note.ts30
-rw-r--r--src/remote/activitypub/kernel/follow.ts24
-rw-r--r--src/remote/activitypub/kernel/index.ts51
-rw-r--r--src/remote/activitypub/kernel/like.ts20
-rw-r--r--src/remote/activitypub/kernel/undo/follow.ts24
-rw-r--r--src/remote/activitypub/kernel/undo/index.ts37
-rw-r--r--src/remote/activitypub/objects/image.ts36
-rw-r--r--src/remote/activitypub/objects/note.ts110
-rw-r--r--src/remote/activitypub/objects/person.ts142
-rw-r--r--src/remote/activitypub/perform.ts7
-rw-r--r--src/remote/activitypub/renderer/accept.ts4
-rw-r--r--src/remote/activitypub/renderer/announce.ts4
-rw-r--r--src/remote/activitypub/renderer/context.ts5
-rw-r--r--src/remote/activitypub/renderer/create.ts4
-rw-r--r--src/remote/activitypub/renderer/document.ts7
-rw-r--r--src/remote/activitypub/renderer/follow.ts8
-rw-r--r--src/remote/activitypub/renderer/hashtag.ts7
-rw-r--r--src/remote/activitypub/renderer/image.ts6
-rw-r--r--src/remote/activitypub/renderer/key.ts10
-rw-r--r--src/remote/activitypub/renderer/like.ts8
-rw-r--r--src/remote/activitypub/renderer/note.ts59
-rw-r--r--src/remote/activitypub/renderer/ordered-collection.ts6
-rw-r--r--src/remote/activitypub/renderer/person.ts21
-rw-r--r--src/remote/activitypub/renderer/undo.ts4
-rw-r--r--src/remote/activitypub/request.ts44
-rw-r--r--src/remote/activitypub/resolver.ts70
-rw-r--r--src/remote/activitypub/type.ts100
33 files changed, 1023 insertions, 0 deletions
diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts
new file mode 100644
index 0000000000..a2cf2d5762
--- /dev/null
+++ b/src/remote/activitypub/kernel/announce/index.ts
@@ -0,0 +1,35 @@
+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<void> => {
+ 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/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
new file mode 100644
index 0000000000..68fb23c97f
--- /dev/null
+++ b/src/remote/activitypub/kernel/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<void> {
+ 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/kernel/create/image.ts b/src/remote/activitypub/kernel/create/image.ts
new file mode 100644
index 0000000000..ea36545f0c
--- /dev/null
+++ b/src/remote/activitypub/kernel/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<void> {
+ await createImage(image.url, actor);
+}
diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts
new file mode 100644
index 0000000000..e11bcac811
--- /dev/null
+++ b/src/remote/activitypub/kernel/create/index.ts
@@ -0,0 +1,40 @@
+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<void> => {
+ 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/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts
new file mode 100644
index 0000000000..530cf6483f
--- /dev/null
+++ b/src/remote/activitypub/kernel/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<void> {
+ const exist = await fetchNote(note);
+ if (exist == null) {
+ await createNote(note);
+ }
+}
diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts
new file mode 100644
index 0000000000..10b47dc4ca
--- /dev/null
+++ b/src/remote/activitypub/kernel/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<void> => {
+ 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/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts
new file mode 100644
index 0000000000..64c342d39b
--- /dev/null
+++ b/src/remote/activitypub/kernel/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<void> {
+ 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/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts
new file mode 100644
index 0000000000..6a8b5a1bec
--- /dev/null
+++ b/src/remote/activitypub/kernel/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<void> => {
+ 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/kernel/index.ts b/src/remote/activitypub/kernel/index.ts
new file mode 100644
index 0000000000..15ea9494ae
--- /dev/null
+++ b/src/remote/activitypub/kernel/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<void> => {
+ 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/kernel/like.ts b/src/remote/activitypub/kernel/like.ts
new file mode 100644
index 0000000000..4941608588
--- /dev/null
+++ b/src/remote/activitypub/kernel/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/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts
new file mode 100644
index 0000000000..a85cb0305d
--- /dev/null
+++ b/src/remote/activitypub/kernel/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<void> => {
+ 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/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts
new file mode 100644
index 0000000000..71f547aeb9
--- /dev/null
+++ b/src/remote/activitypub/kernel/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<void> => {
+ 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..d7bc5aff2f
--- /dev/null
+++ b/src/remote/activitypub/objects/image.ts
@@ -0,0 +1,36 @@
+import * as debug from 'debug';
+
+import uploadFromUrl from '../../../services/drive/upload-from-url';
+import { IRemoteUser } from '../../../models/user';
+import { IDriveFile } from '../../../models/drive-file';
+import Resolver from '../resolver';
+
+const log = debug('misskey:activitypub');
+
+/**
+ * Imageを作成します。
+ */
+export async function createImage(actor: IRemoteUser, value): Promise<IDriveFile> {
+ const image = await new Resolver().resolve(value);
+
+ if (image.url == null) {
+ throw new Error('invalid image: url not privided');
+ }
+
+ 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<IDriveFile> {
+ // 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<INote> {
+ 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<INote> {
+ 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<INote> {
+ 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..f7ec064cdb
--- /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, 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<IUser> {
+ 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<IUser> {
+ 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)
+ ) {
+ log(`invalid person: ${JSON.stringify(object, null, 2)}`);
+ 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)
+ ))).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<IUser> {
+ 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.ts b/src/remote/activitypub/perform.ts
new file mode 100644
index 0000000000..2e4f53adf5
--- /dev/null
+++ b/src/remote/activitypub/perform.ts
@@ -0,0 +1,7 @@
+import { Object } from './type';
+import { IRemoteUser } from '../../models/user';
+import kernel from './kernel';
+
+export default async (actor: IRemoteUser, activity: Object): Promise<void> => {
+ await kernel(actor, activity);
+};
diff --git a/src/remote/activitypub/renderer/accept.ts b/src/remote/activitypub/renderer/accept.ts
new file mode 100644
index 0000000000..00c76883a9
--- /dev/null
+++ b/src/remote/activitypub/renderer/accept.ts
@@ -0,0 +1,4 @@
+export default object => ({
+ type: 'Accept',
+ object
+});
diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts
new file mode 100644
index 0000000000..8e4b3d26a6
--- /dev/null
+++ b/src/remote/activitypub/renderer/announce.ts
@@ -0,0 +1,4 @@
+export default object => ({
+ type: 'Announce',
+ object
+});
diff --git a/src/remote/activitypub/renderer/context.ts b/src/remote/activitypub/renderer/context.ts
new file mode 100644
index 0000000000..b56f727ae7
--- /dev/null
+++ b/src/remote/activitypub/renderer/context.ts
@@ -0,0 +1,5 @@
+export default [
+ 'https://www.w3.org/ns/activitystreams',
+ 'https://w3id.org/security/v1',
+ { Hashtag: 'as:Hashtag' }
+];
diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts
new file mode 100644
index 0000000000..de411e1951
--- /dev/null
+++ b/src/remote/activitypub/renderer/create.ts
@@ -0,0 +1,4 @@
+export default object => ({
+ type: 'Create',
+ object
+});
diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts
new file mode 100644
index 0000000000..91a9f7df38
--- /dev/null
+++ b/src/remote/activitypub/renderer/document.ts
@@ -0,0 +1,7 @@
+import config from '../../../config';
+
+export default ({ _id, contentType }) => ({
+ type: 'Document',
+ mediaType: contentType,
+ url: `${config.drive_url}/${_id}`
+});
diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts
new file mode 100644
index 0000000000..bf8eeff06b
--- /dev/null
+++ b/src/remote/activitypub/renderer/follow.ts
@@ -0,0 +1,8 @@
+import config from '../../../config';
+import { IRemoteUser, ILocalUser } from '../../../models/user';
+
+export default (follower: ILocalUser, followee: IRemoteUser) => ({
+ type: 'Follow',
+ actor: `${config.url}/users/${follower._id}`,
+ object: followee.uri
+});
diff --git a/src/remote/activitypub/renderer/hashtag.ts b/src/remote/activitypub/renderer/hashtag.ts
new file mode 100644
index 0000000000..cf0b07b48a
--- /dev/null
+++ b/src/remote/activitypub/renderer/hashtag.ts
@@ -0,0 +1,7 @@
+import config from '../../../config';
+
+export default tag => ({
+ type: 'Hashtag',
+ href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
+ name: '#' + tag
+});
diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts
new file mode 100644
index 0000000000..d671a57e7c
--- /dev/null
+++ b/src/remote/activitypub/renderer/image.ts
@@ -0,0 +1,6 @@
+import config from '../../../config';
+
+export default ({ _id }) => ({
+ type: 'Image',
+ url: `${config.drive_url}/${_id}`
+});
diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts
new file mode 100644
index 0000000000..0d5e52557c
--- /dev/null
+++ b/src/remote/activitypub/renderer/key.ts
@@ -0,0 +1,10 @@
+import config from '../../../config';
+import { extractPublic } from '../../../crypto_key';
+import { ILocalUser } from '../../../models/user';
+
+export default (user: ILocalUser) => ({
+ id: `${config.url}/users/${user._id}/publickey`,
+ type: 'Key',
+ owner: `${config.url}/users/${user._id}`,
+ publicKeyPem: extractPublic(user.keypair)
+});
diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts
new file mode 100644
index 0000000000..061a10ba84
--- /dev/null
+++ b/src/remote/activitypub/renderer/like.ts
@@ -0,0 +1,8 @@
+import config from '../../../config';
+import { ILocalUser } from '../../../models/user';
+
+export default (user: ILocalUser, note) => ({
+ type: 'Like',
+ actor: `${config.url}/users/${user._id}`,
+ object: note.uri ? note.uri : `${config.url}/notes/${note._id}`
+});
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
new file mode 100644
index 0000000000..c364b13249
--- /dev/null
+++ b/src/remote/activitypub/renderer/note.ts
@@ -0,0 +1,59 @@
+import renderDocument from './document';
+import renderHashtag from './hashtag';
+import config from '../../../config';
+import DriveFile from '../../../models/drive-file';
+import Note, { INote } from '../../../models/note';
+import User from '../../../models/user';
+
+export default async function renderNote(note: INote, dive = true) {
+ const promisedFiles = note.mediaIds
+ ? DriveFile.find({ _id: { $in: note.mediaIds } })
+ : Promise.resolve([]);
+
+ let inReplyTo;
+
+ if (note.replyId) {
+ const inReplyToNote = await Note.findOne({
+ _id: note.replyId,
+ });
+
+ if (inReplyToNote !== null) {
+ const inReplyToUser = await User.findOne({
+ _id: inReplyToNote.userId,
+ });
+
+ if (inReplyToUser !== null) {
+ if (inReplyToNote.uri) {
+ inReplyTo = inReplyToNote.uri;
+ } else {
+ if (dive) {
+ inReplyTo = await renderNote(inReplyToNote, false);
+ } else {
+ inReplyTo = `${config.url}/notes/${inReplyToNote._id}`;
+ }
+ }
+ }
+ }
+ } else {
+ inReplyTo = null;
+ }
+
+ const user = await User.findOne({
+ _id: note.userId
+ });
+
+ const attributedTo = `${config.url}/users/${user._id}`;
+
+ return {
+ id: `${config.url}/notes/${note._id}`,
+ type: 'Note',
+ attributedTo,
+ content: note.textHtml,
+ published: note.createdAt.toISOString(),
+ to: 'https://www.w3.org/ns/activitystreams#Public',
+ cc: `${attributedTo}/followers`,
+ inReplyTo,
+ attachment: (await promisedFiles).map(renderDocument),
+ tag: (note.tags || []).map(renderHashtag)
+ };
+}
diff --git a/src/remote/activitypub/renderer/ordered-collection.ts b/src/remote/activitypub/renderer/ordered-collection.ts
new file mode 100644
index 0000000000..2ca0f77354
--- /dev/null
+++ b/src/remote/activitypub/renderer/ordered-collection.ts
@@ -0,0 +1,6 @@
+export default (id, totalItems, orderedItems) => ({
+ id,
+ type: 'OrderedCollection',
+ totalItems,
+ orderedItems
+});
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
new file mode 100644
index 0000000000..f1c8056a75
--- /dev/null
+++ b/src/remote/activitypub/renderer/person.ts
@@ -0,0 +1,21 @@
+import renderImage from './image';
+import renderKey from './key';
+import config from '../../../config';
+
+export default user => {
+ const id = `${config.url}/users/${user._id}`;
+
+ return {
+ type: 'Person',
+ id,
+ inbox: `${id}/inbox`,
+ outbox: `${id}/outbox`,
+ url: `${config.url}/@${user.username}`,
+ preferredUsername: user.username,
+ name: user.name,
+ summary: user.description,
+ icon: user.avatarId && renderImage({ _id: user.avatarId }),
+ image: user.bannerId && renderImage({ _id: user.bannerId }),
+ publicKey: renderKey(user)
+ };
+};
diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts
new file mode 100644
index 0000000000..f38e224b60
--- /dev/null
+++ b/src/remote/activitypub/renderer/undo.ts
@@ -0,0 +1,4 @@
+export default object => ({
+ type: 'Undo',
+ object
+});
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
new file mode 100644
index 0000000000..85f43eb91d
--- /dev/null
+++ b/src/remote/activitypub/request.ts
@@ -0,0 +1,44 @@
+import { request } from 'https';
+import { sign } from 'http-signature';
+import { URL } from 'url';
+import * as debug from 'debug';
+
+import config from '../../config';
+import { ILocalUser } from '../../models/user';
+
+const log = debug('misskey:activitypub:deliver');
+
+export default (user: ILocalUser, url: string, object) => new Promise((resolve, reject) => {
+ log(`--> ${url}`);
+
+ const { protocol, hostname, port, pathname, search } = new URL(url);
+
+ const req = request({
+ protocol,
+ hostname,
+ port,
+ method: 'POST',
+ path: pathname + search,
+ }, res => {
+ res.on('end', () => {
+ log(`${url} --> ${res.statusCode}`);
+
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ resolve();
+ } else {
+ reject(res);
+ }
+ });
+
+ res.on('data', () => {});
+ res.on('error', reject);
+ });
+
+ sign(req, {
+ authorizationHeaderName: 'Signature',
+ key: user.keypair,
+ keyId: `acct:${user.username}@${config.host}`
+ });
+
+ req.end(JSON.stringify(object));
+});
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
new file mode 100644
index 0000000000..f405ff10c3
--- /dev/null
+++ b/src/remote/activitypub/resolver.ts
@@ -0,0 +1,70 @@
+import * as request from 'request-promise-native';
+import * as debug from 'debug';
+import { IObject } from './type';
+//import config from '../../config';
+
+const log = debug('misskey:activitypub:resolver');
+
+export default class Resolver {
+ private history: Set<string>;
+
+ constructor() {
+ this.history = new Set();
+ }
+
+ public async resolveCollection(value) {
+ const collection = typeof value === 'string'
+ ? await this.resolve(value)
+ : value;
+
+ switch (collection.type) {
+ case 'Collection':
+ collection.objects = collection.object.items;
+ break;
+
+ case 'OrderedCollection':
+ collection.objects = collection.object.orderedItems;
+ break;
+
+ default:
+ throw new Error(`unknown collection type: ${collection.type}`);
+ }
+
+ return collection;
+ }
+
+ public async resolve(value): Promise<IObject> {
+ if (value == null) {
+ throw new Error('resolvee is null (or undefined)');
+ }
+
+ if (typeof value !== 'string') {
+ return value;
+ }
+
+ if (this.history.has(value)) {
+ throw new Error('cannot resolve already resolved one');
+ }
+
+ this.history.add(value);
+
+ const object = await request({
+ url: value,
+ headers: {
+ Accept: 'application/activity+json, application/ld+json'
+ },
+ json: true
+ });
+
+ if (object === null || (
+ Array.isArray(object['@context']) ?
+ !object['@context'].includes('https://www.w3.org/ns/activitystreams') :
+ object['@context'] !== 'https://www.w3.org/ns/activitystreams'
+ )) {
+ log(`invalid response: ${value}`);
+ throw new Error('invalid response');
+ }
+
+ return object;
+ }
+}
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
new file mode 100644
index 0000000000..08e5493dd4
--- /dev/null
+++ b/src/remote/activitypub/type.ts
@@ -0,0 +1,100 @@
+export type obj = { [x: string]: any };
+
+export interface IObject {
+ '@context': string | obj | obj[];
+ type: string;
+ id?: string;
+ summary?: string;
+ published?: string;
+ cc?: string[];
+ to?: string[];
+ attributedTo: string;
+ attachment?: any[];
+ inReplyTo?: any;
+ content: string;
+ icon?: any;
+ image?: any;
+ url?: string;
+}
+
+export interface IActivity extends IObject {
+ //type: 'Activity';
+ actor: IObject | string;
+ object: IObject | string;
+ target?: IObject | string;
+}
+
+export interface ICollection extends IObject {
+ type: 'Collection';
+ totalItems: number;
+ items: IObject | string | IObject[] | string[];
+}
+
+export interface IOrderedCollection extends IObject {
+ type: 'OrderedCollection';
+ totalItems: number;
+ orderedItems: IObject | string | IObject[] | string[];
+}
+
+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';
+
+export const isOrderedCollection = (object: IObject): object is IOrderedCollection =>
+ object.type === 'OrderedCollection';
+
+export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
+ isCollection(object) || isOrderedCollection(object);
+
+export interface ICreate extends IActivity {
+ type: 'Create';
+}
+
+export interface IDelete extends IActivity {
+ type: 'Delete';
+}
+
+export interface IUndo extends IActivity {
+ type: 'Undo';
+}
+
+export interface IFollow extends IActivity {
+ type: 'Follow';
+}
+
+export interface IAccept extends IActivity {
+ type: 'Accept';
+}
+
+export interface ILike extends IActivity {
+ type: 'Like';
+}
+
+export interface IAnnounce extends IActivity {
+ type: 'Announce';
+}
+
+export type Object =
+ ICollection |
+ IOrderedCollection |
+ ICreate |
+ IDelete |
+ IUndo |
+ IFollow |
+ IAccept |
+ ILike |
+ IAnnounce;