summaryrefslogtreecommitdiff
path: root/src/remote/activitypub
diff options
context:
space:
mode:
authorrinsuki <428rinsuki+git@gmail.com>2018-05-17 07:52:24 +0900
committerrinsuki <428rinsuki+git@gmail.com>2018-05-17 07:52:24 +0900
commit829b4012e6dc14eb64a3d8f60826fe9b6a41b40d (patch)
tree42ac37f323db349dca9316e6fdb39fc33b860686 /src/remote/activitypub
parentadd yarn.lock to gitignore (diff)
parentUpdate deliver.ts (diff)
downloadmisskey-829b4012e6dc14eb64a3d8f60826fe9b6a41b40d.tar.gz
misskey-829b4012e6dc14eb64a3d8f60826fe9b6a41b40d.tar.bz2
misskey-829b4012e6dc14eb64a3d8f60826fe9b6a41b40d.zip
Merge branch 'master' into fix/yarn-lock-ignore
Diffstat (limited to 'src/remote/activitypub')
-rw-r--r--src/remote/activitypub/kernel/announce/note.ts15
-rw-r--r--src/remote/activitypub/kernel/delete/note.ts1
-rw-r--r--src/remote/activitypub/kernel/follow.ts5
-rw-r--r--src/remote/activitypub/kernel/like.ts15
-rw-r--r--src/remote/activitypub/kernel/undo/follow.ts5
-rw-r--r--src/remote/activitypub/misc/get-note-html.ts23
-rw-r--r--src/remote/activitypub/models/image.ts7
-rw-r--r--src/remote/activitypub/models/note.ts104
-rw-r--r--src/remote/activitypub/models/person.ts62
-rw-r--r--src/remote/activitypub/renderer/like.ts5
-rw-r--r--src/remote/activitypub/renderer/note.ts3
-rw-r--r--src/remote/activitypub/renderer/person.ts1
-rw-r--r--src/remote/activitypub/request.ts5
-rw-r--r--src/remote/activitypub/type.ts3
14 files changed, 209 insertions, 45 deletions
diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
index a288dd499a..fe645b07b5 100644
--- a/src/remote/activitypub/kernel/announce/note.ts
+++ b/src/remote/activitypub/kernel/announce/note.ts
@@ -5,6 +5,7 @@ import post from '../../../../services/note/create';
import { IRemoteUser } from '../../../../models/user';
import { IAnnounce, INote } from '../../type';
import { fetchNote, resolveNote } from '../../models/note';
+import { resolvePerson } from '../../models/person';
const log = debug('misskey:activitypub');
@@ -30,16 +31,22 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
//#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');
+ let visibleUsers = [];
+ if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
+ if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
+ visibility = 'home';
+ } else {
+ visibility = 'specified';
+ visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
+ }
+ } if (activity.cc.length == 0) visibility = 'followers';
//#endergion
await post(actor, {
createdAt: new Date(activity.published),
renote,
visibility,
+ visibleUsers,
uri
});
}
diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts
index 64c342d39b..b2868f69a3 100644
--- a/src/remote/activitypub/kernel/delete/note.ts
+++ b/src/remote/activitypub/kernel/delete/note.ts
@@ -22,7 +22,6 @@ export default async function(actor: IRemoteUser, uri: string): Promise<void> {
$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
index 6a8b5a1bec..7e31eb32ea 100644
--- a/src/remote/activitypub/kernel/follow.ts
+++ b/src/remote/activitypub/kernel/follow.ts
@@ -1,3 +1,4 @@
+import * as mongo from 'mongodb';
import User, { IRemoteUser } from '../../../models/user';
import config from '../../../config';
import follow from '../../../services/following/create';
@@ -10,7 +11,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
return null;
}
- const followee = await User.findOne({ _id: id.split('/').pop() });
+ const followee = await User.findOne({
+ _id: new mongo.ObjectID(id.split('/').pop())
+ });
if (followee === null) {
throw new Error('followee not found');
diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts
index 4941608588..17ec73f12b 100644
--- a/src/remote/activitypub/kernel/like.ts
+++ b/src/remote/activitypub/kernel/like.ts
@@ -1,7 +1,9 @@
+import * as mongo from 'mongodb';
import Note from '../../../models/note';
import { IRemoteUser } from '../../../models/user';
import { ILike } from '../type';
import create from '../../../services/note/reaction/create';
+import { validateReaction } from '../../../models/note-reaction';
export default async (actor: IRemoteUser, activity: ILike) => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@@ -9,12 +11,21 @@ export default async (actor: IRemoteUser, activity: ILike) => {
// Transform:
// https://misskey.ex/notes/xxxx to
// xxxx
- const noteId = id.split('/').pop();
+ const noteId = new mongo.ObjectID(id.split('/').pop());
const note = await Note.findOne({ _id: noteId });
if (note === null) {
throw new Error();
}
- await create(actor, note, 'pudding');
+ let reaction = 'pudding';
+
+ // 他のMisskeyインスタンスからのリアクション
+ if (activity._misskey_reaction) {
+ if (validateReaction.ok(activity._misskey_reaction)) {
+ reaction = activity._misskey_reaction;
+ }
+ }
+
+ await create(actor, note, reaction);
};
diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts
index a85cb0305d..c0b10c1898 100644
--- a/src/remote/activitypub/kernel/undo/follow.ts
+++ b/src/remote/activitypub/kernel/undo/follow.ts
@@ -1,3 +1,4 @@
+import * as mongo from 'mongodb';
import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import unfollow from '../../../../services/following/delete';
@@ -10,7 +11,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
return null;
}
- const followee = await User.findOne({ _id: id.split('/').pop() });
+ const followee = await User.findOne({
+ _id: new mongo.ObjectID(id.split('/').pop())
+ });
if (followee === null) {
throw new Error('followee not found');
diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts
new file mode 100644
index 0000000000..5bca4eed62
--- /dev/null
+++ b/src/remote/activitypub/misc/get-note-html.ts
@@ -0,0 +1,23 @@
+import { INote } from "../../../models/note";
+import toHtml from '../../../text/html';
+import parse from '../../../text/parse';
+import config from '../../../config';
+
+export default function(note: INote) {
+ if (note.text == null) return null;
+
+ let html = toHtml(parse(note.text));
+
+ if (note.poll != null) {
+ const url = `${config.url}/notes/${note._id}`;
+ // TODO: i18n
+ html += `<p><a href="${url}">【Misskeyで投票を見る】</a></p>`;
+ }
+
+ if (note.renoteId != null) {
+ const url = `${config.url}/notes/${note.renoteId}`;
+ html += `<p>RE: <a href="${url}">${url}</a></p>`;
+ }
+
+ return html;
+}
diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts
index d7bc5aff2f..0d5a690c6c 100644
--- a/src/remote/activitypub/models/image.ts
+++ b/src/remote/activitypub/models/image.ts
@@ -11,6 +11,11 @@ const log = debug('misskey:activitypub');
* Imageを作成します。
*/
export async function createImage(actor: IRemoteUser, value): Promise<IDriveFile> {
+ // 投稿者が凍結されていたらスキップ
+ if (actor.isSuspended) {
+ return null;
+ }
+
const image = await new Resolver().resolve(value);
if (image.url == null) {
@@ -19,7 +24,7 @@ export async function createImage(actor: IRemoteUser, value): Promise<IDriveFile
log(`Creating the Image: ${image.url}`);
- return await uploadFromUrl(image.url, actor);
+ return await uploadFromUrl(image.url, actor, null, image.url);
}
/**
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index ab6dd99a77..91e700ef6f 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -1,4 +1,5 @@
-import { JSDOM } from 'jsdom';
+import * as mongo from 'mongodb';
+import * as parse5 from 'parse5';
import * as debug from 'debug';
import config from '../../../config';
@@ -12,6 +13,76 @@ import { IRemoteUser } from '../../../models/user';
const log = debug('misskey:activitypub');
+function parse(html: string): string {
+ const dom = parse5.parseFragment(html) as parse5.AST.Default.Document;
+
+ let text = '';
+
+ dom.childNodes.forEach(n => analyze(n));
+
+ return text.trim();
+
+ function getText(node) {
+ if (node.nodeName == '#text') return node.value;
+
+ if (node.childNodes) {
+ return node.childNodes.map(n => getText(n)).join('');
+ }
+
+ return '';
+ }
+
+ function analyze(node) {
+ switch (node.nodeName) {
+ case '#text':
+ text += node.value;
+ break;
+
+ case 'br':
+ text += '\n';
+ break;
+
+ case 'a':
+ const txt = getText(node);
+
+ // メンション
+ if (txt.startsWith('@')) {
+ const part = txt.split('@');
+
+ if (part.length == 2) {
+ //#region ホスト名部分が省略されているので復元する
+ const href = new URL(node.attrs.find(x => x.name == 'href').value);
+ const acct = txt + '@' + href.hostname;
+ text += acct;
+ break;
+ //#endregion
+ } else if (part.length == 3) {
+ text += txt;
+ break;
+ }
+ }
+
+ if (node.childNodes) {
+ node.childNodes.forEach(n => analyze(n));
+ }
+ break;
+
+ case 'p':
+ text += '\n\n';
+ if (node.childNodes) {
+ node.childNodes.forEach(n => analyze(n));
+ }
+ break;
+
+ default:
+ if (node.childNodes) {
+ node.childNodes.forEach(n => analyze(n));
+ }
+ break;
+ }
+ }
+}
+
/**
* Noteをフェッチします。
*
@@ -22,7 +93,8 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) {
- return await Note.findOne({ _id: uri.split('/').pop() });
+ const id = new mongo.ObjectID(uri.split('/').pop());
+ return await Note.findOne({ _id: id });
}
//#region このサーバーに既に登録されていたらそれを返す
@@ -45,7 +117,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const object = await resolver.resolve(value) as any;
if (object == null || object.type !== 'Note') {
- throw new Error('invalid note');
+ log(`invalid note: ${object}`);
+ return null;
}
const note: INoteActivityStreamsObject = object;
@@ -55,12 +128,23 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
// 投稿者をフェッチ
const actor = await resolvePerson(note.attributedTo) as IRemoteUser;
+ // 投稿者が凍結されていたらスキップ
+ if (actor.isSuspended) {
+ return null;
+ }
+
//#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');
+ let visibleUsers = [];
+ if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
+ if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
+ visibility = 'home';
+ } else {
+ visibility = 'specified';
+ visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
+ }
+ }
+ if (note.cc.length == 0) visibility = 'followers';
//#endergion
// 添付メディア
@@ -73,7 +157,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
// リプライ
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
- const { window } = new JSDOM(note.content);
+ // テキストのパース
+ const text = parse(note.content);
// ユーザーの情報が古かったらついでに更新しておく
if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
@@ -85,10 +170,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
media,
reply,
renote: undefined,
- text: window.document.body.textContent,
+ text: text,
viaMobile: false,
geo: undefined,
visibility,
+ visibleUsers,
uri: note.id
}, silent);
}
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index b755b2603a..33280f3d89 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -1,3 +1,4 @@
+import * as mongo from 'mongodb';
import { JSDOM } from 'jsdom';
import { toUnicode } from 'punycode';
import * as debug from 'debug';
@@ -21,7 +22,8 @@ export async function fetchPerson(value: string | IObject, resolver?: Resolver):
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) {
- return await User.findOne({ _id: uri.split('/').pop() });
+ const id = new mongo.ObjectID(uri.split('/').pop());
+ return await User.findOne({ _id: id });
}
//#region このサーバーに既に登録されていたらそれを返す
@@ -47,6 +49,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
object == null ||
object.type !== 'Person' ||
typeof object.preferredUsername !== 'string' ||
+ typeof object.inbox !== 'string' ||
!validateUsername(object.preferredUsername) ||
!isValidName(object.name == '' ? null : object.name)
) {
@@ -78,27 +81,39 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
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,
- publicKey: {
- id: person.publicKey.id,
- publicKeyPem: person.publicKey.publicKeyPem
- },
- inbox: person.inbox,
- uri: person.id,
- url: person.url
- }) as IRemoteUser;
+ let user: IRemoteUser;
+ try {
+ 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,
+ publicKey: {
+ id: person.publicKey.id,
+ publicKeyPem: person.publicKey.publicKeyPem
+ },
+ inbox: person.inbox,
+ endpoints: person.endpoints,
+ uri: person.id,
+ url: person.url
+ }) as IRemoteUser;
+ } catch (e) {
+ // duplicate key error
+ if (e.code === 11000) {
+ throw new Error('already registered');
+ }
+
+ console.error(e);
+ throw e;
+ }
//#region アイコンとヘッダー画像をフェッチ
const [avatarId, bannerId] = (await Promise.all([
@@ -194,7 +209,8 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
followingCount,
notesCount,
name: person.name,
- url: person.url
+ url: person.url,
+ endpoints: person.endpoints
}
});
}
diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts
index 061a10ba84..33e1341a20 100644
--- a/src/remote/activitypub/renderer/like.ts
+++ b/src/remote/activitypub/renderer/like.ts
@@ -1,8 +1,9 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
-export default (user: ILocalUser, note) => ({
+export default (user: ILocalUser, note, reaction: string) => ({
type: 'Like',
actor: `${config.url}/users/${user._id}`,
- object: note.uri ? note.uri : `${config.url}/notes/${note._id}`
+ object: note.uri ? note.uri : `${config.url}/notes/${note._id}`,
+ _misskey_reaction: reaction
});
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index c364b13249..a05c12b388 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -4,6 +4,7 @@ import config from '../../../config';
import DriveFile from '../../../models/drive-file';
import Note, { INote } from '../../../models/note';
import User from '../../../models/user';
+import toHtml from '../misc/get-note-html';
export default async function renderNote(note: INote, dive = true) {
const promisedFiles = note.mediaIds
@@ -48,7 +49,7 @@ export default async function renderNote(note: INote, dive = true) {
id: `${config.url}/notes/${note._id}`,
type: 'Note',
attributedTo,
- content: note.textHtml,
+ content: toHtml(note),
published: note.createdAt.toISOString(),
to: 'https://www.w3.org/ns/activitystreams#Public',
cc: `${attributedTo}/followers`,
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index f1c8056a75..424305f8d3 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -10,6 +10,7 @@ export default user => {
id,
inbox: `${id}/inbox`,
outbox: `${id}/outbox`,
+ sharedInbox: `${config.url}/inbox`,
url: `${config.url}/@${user.username}`,
preferredUsername: user.username,
name: user.name,
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
index 85f43eb91d..e6861fdb3e 100644
--- a/src/remote/activitypub/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -40,5 +40,10 @@ export default (user: ILocalUser, url: string, object) => new Promise((resolve,
keyId: `acct:${user.username}@${config.host}`
});
+ // Signature: Signature ... => Signature: ...
+ let sig = req.getHeader('Signature').toString();
+ sig = sig.replace(/^Signature /, '');
+ req.setHeader('Signature', sig);
+
req.end(JSON.stringify(object));
});
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index 08e5493dd4..ca38ec2227 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -15,6 +15,7 @@ export interface IObject {
icon?: any;
image?: any;
url?: string;
+ tag?: any[];
}
export interface IActivity extends IObject {
@@ -49,6 +50,7 @@ export interface IPerson extends IObject {
followers: any;
following: any;
outbox: any;
+ endpoints: string[];
}
export const isCollection = (object: IObject): object is ICollection =>
@@ -82,6 +84,7 @@ export interface IAccept extends IActivity {
export interface ILike extends IActivity {
type: 'Like';
+ _misskey_reaction: string;
}
export interface IAnnounce extends IActivity {