diff options
| author | rinsuki <428rinsuki+git@gmail.com> | 2018-05-17 07:52:24 +0900 |
|---|---|---|
| committer | rinsuki <428rinsuki+git@gmail.com> | 2018-05-17 07:52:24 +0900 |
| commit | 829b4012e6dc14eb64a3d8f60826fe9b6a41b40d (patch) | |
| tree | 42ac37f323db349dca9316e6fdb39fc33b860686 /src/remote/activitypub | |
| parent | add yarn.lock to gitignore (diff) | |
| parent | Update deliver.ts (diff) | |
| download | misskey-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.ts | 15 | ||||
| -rw-r--r-- | src/remote/activitypub/kernel/delete/note.ts | 1 | ||||
| -rw-r--r-- | src/remote/activitypub/kernel/follow.ts | 5 | ||||
| -rw-r--r-- | src/remote/activitypub/kernel/like.ts | 15 | ||||
| -rw-r--r-- | src/remote/activitypub/kernel/undo/follow.ts | 5 | ||||
| -rw-r--r-- | src/remote/activitypub/misc/get-note-html.ts | 23 | ||||
| -rw-r--r-- | src/remote/activitypub/models/image.ts | 7 | ||||
| -rw-r--r-- | src/remote/activitypub/models/note.ts | 104 | ||||
| -rw-r--r-- | src/remote/activitypub/models/person.ts | 62 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/like.ts | 5 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/note.ts | 3 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/person.ts | 1 | ||||
| -rw-r--r-- | src/remote/activitypub/request.ts | 5 | ||||
| -rw-r--r-- | src/remote/activitypub/type.ts | 3 |
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 { |