diff options
Diffstat (limited to 'src/remote')
21 files changed, 444 insertions, 491 deletions
diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts deleted file mode 100644 index fa681982cf..0000000000 --- a/src/remote/activitypub/act/create.ts +++ /dev/null @@ -1,10 +0,0 @@ -import create from '../create'; -import Resolver from '../resolver'; - -export default (resolver: Resolver, actor, activity, distribute) => { - if ('actor' in activity && actor.account.uri !== activity.actor) { - throw new Error(); - } - - return create(resolver, actor, activity.object, distribute); -}; diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts new file mode 100644 index 0000000000..30a75e7377 --- /dev/null +++ b/src/remote/activitypub/act/create/image.ts @@ -0,0 +1,18 @@ +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<IDriveFile> { + if ('attributedTo' in image && actor.account.uri !== image.attributedTo) { + log(`invalid image: ${JSON.stringify(image, null, 2)}`); + throw new Error('invalid image'); + } + + log(`Creating the Image: ${image.id}`); + + return await uploadFromUrl(image.url, actor); +} diff --git a/src/remote/activitypub/act/create/index.ts b/src/remote/activitypub/act/create/index.ts new file mode 100644 index 0000000000..dd0b112141 --- /dev/null +++ b/src/remote/activitypub/act/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<void> => { + if ('actor' in activity && actor.account.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 new file mode 100644 index 0000000000..82a6207038 --- /dev/null +++ b/src/remote/activitypub/act/create/note.ts @@ -0,0 +1,89 @@ +import { JSDOM } from 'jsdom'; +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import Post, { IPost } from '../../../../models/post'; +import createPost from '../../../../services/post/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<IPost> { + if (typeof note.id !== 'string') { + log(`invalid note: ${JSON.stringify(note, null, 2)}`); + throw new Error('invalid note'); + } + + // 既に同じURIを持つものが登録されていないかチェックし、登録されていたらそれを返す + const exist = await Post.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 添付メディア + const media = []; + if ('attachment' in note && note.attachment != null) { + // TODO: attachmentは必ずしもImageではない + // TODO: attachmentは必ずしも配列ではない + // TODO: ループの中でawaitはすべきでない + note.attachment.forEach(async media => { + const created = await createImage(note.actor, media); + media.push(created); + }); + } + //#endregion + + //#region リプライ + let reply = null; + if ('inReplyTo' in note && note.inReplyTo != null) { + // リプライ先の投稿がMisskeyに登録されているか調べる + const uri: string = note.inReplyTo.id || note.inReplyTo; + const inReplyToPost = uri.startsWith(config.url + '/') + ? await Post.findOne({ _id: uri.split('/').pop() }) + : await Post.findOne({ uri }); + + if (inReplyToPost) { + reply = inReplyToPost; + } 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 createPost(actor, { + createdAt: new Date(note.published), + media, + reply, + repost: undefined, + text: window.document.body.textContent, + viaMobile: false, + geo: undefined, + visibility, + uri: note.id + }); +} diff --git a/src/remote/activitypub/act/delete.ts b/src/remote/activitypub/act/delete.ts deleted file mode 100644 index f9eb4dd08d..0000000000 --- a/src/remote/activitypub/act/delete.ts +++ /dev/null @@ -1,21 +0,0 @@ -import create from '../create'; -import deleteObject from '../delete'; - -export default async (resolver, actor, activity) => { - if ('actor' in activity && actor.account.uri !== activity.actor) { - throw new Error(); - } - - const results = await create(resolver, actor, activity.object); - - await Promise.all(results.map(async promisedResult => { - const result = await promisedResult; - if (result === null) { - return; - } - - await deleteObject(result); - })); - - return null; -}; diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts new file mode 100644 index 0000000000..e34577b310 --- /dev/null +++ b/src/remote/activitypub/act/delete/index.ts @@ -0,0 +1,36 @@ +import Resolver from '../../resolver'; +import deleteNote from './note'; +import Post from '../../../../models/post'; +import { IRemoteUser } from '../../../../models/user'; + +/** + * 削除アクティビティを捌きます + */ +export default async (actor: IRemoteUser, activity): Promise<void> => { + if ('actor' in activity && actor.account.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 post = await Post.findOne({ uri }); + if (post != 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 new file mode 100644 index 0000000000..8e9447b481 --- /dev/null +++ b/src/remote/activitypub/act/delete/note.ts @@ -0,0 +1,30 @@ +import * as debug from 'debug'; + +import Post from '../../../../models/post'; +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 post = await Post.findOne({ uri }); + + if (post == null) { + throw new Error('post not found'); + } + + if (!post.userId.equals(actor._id)) { + throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); + } + + Post.update({ _id: post._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 index 222a257e1a..3dd029af54 100644 --- a/src/remote/activitypub/act/follow.ts +++ b/src/remote/activitypub/act/follow.ts @@ -1,17 +1,12 @@ -import { MongoError } from 'mongodb'; import parseAcct from '../../../acct/parse'; -import Following, { IFollowing } from '../../../models/following'; -import User from '../../../models/user'; +import User, { IRemoteUser } from '../../../models/user'; import config from '../../../config'; -import { createHttp } from '../../../queue'; -import context from '../renderer/context'; -import renderAccept from '../renderer/accept'; -import request from '../../request'; -import Resolver from '../resolver'; +import follow from '../../../services/following/create'; +import { IFollow } from '../type'; -export default async (resolver: Resolver, actor, activity, distribute) => { +export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const prefix = config.url + '/@'; - const id = activity.object.id || activity.object; + const id = typeof activity == 'string' ? activity : activity.id; if (!id.startsWith(prefix)) { return null; @@ -27,52 +22,5 @@ export default async (resolver: Resolver, actor, activity, distribute) => { throw new Error(); } - if (!distribute) { - const { _id } = await Following.findOne({ - followerId: actor._id, - followeeId: followee._id - }); - - return { - resolver, - object: { $ref: 'following', $id: _id } - }; - } - - const promisedFollowing = Following.insert({ - createdAt: new Date(), - followerId: actor._id, - followeeId: followee._id - }).then(following => new Promise((resolve, reject) => { - createHttp({ - type: 'follow', - following: following._id - }).save(error => { - if (error) { - reject(error); - } else { - resolve(following); - } - }); - }) as Promise<IFollowing>, async error => { - // duplicate key error - if (error instanceof MongoError && error.code === 11000) { - return Following.findOne({ - followerId: actor._id, - followeeId: followee._id - }); - } - - throw error; - }); - - const accept = renderAccept(activity); - accept['@context'] = context; - - await request(followee, actor.account.inbox, accept); - - return promisedFollowing.then(({ _id }) => ({ - resolver, - object: { $ref: 'following', $id: _id } - })); + await follow(actor, followee, activity); }; diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts index d282e12885..5be07c478e 100644 --- a/src/remote/activitypub/act/index.ts +++ b/src/remote/activitypub/act/index.ts @@ -2,35 +2,40 @@ import create from './create'; import performDeleteActivity from './delete'; import follow from './follow'; import undo from './undo'; -import createObject from '../create'; -import Resolver from '../resolver'; +import { IObject } from '../type'; +import { IRemoteUser } from '../../../models/user'; -export default async (parentResolver: Resolver, actor, value, distribute?: boolean) => { - const collection = await parentResolver.resolveCollection(value); +const self = async (actor: IRemoteUser, activity: IObject): Promise<void> => { + switch (activity.type) { + case 'Create': + await create(actor, activity); + break; - return collection.object.map(async element => { - const { resolver, object } = await collection.resolver.resolveOne(element); - const created = await (await createObject(resolver, actor, [object], distribute))[0]; + case 'Delete': + await performDeleteActivity(actor, activity); + break; - if (created !== null) { - return created; - } + case 'Follow': + await follow(actor, activity); + break; - switch (object.type) { - case 'Create': - return create(resolver, actor, object, distribute); + case 'Accept': + // noop + break; - case 'Delete': - return performDeleteActivity(resolver, actor, object); + case 'Undo': + await undo(actor, activity); + break; - case 'Follow': - return follow(resolver, actor, object, distribute); + case 'Collection': + case 'OrderedCollection': + // TODO + break; - case 'Undo': - return undo(resolver, actor, object); - - default: - return null; - } - }); + default: + console.warn(`unknown activity type: ${activity.type}`); + return null; + } }; + +export default self; diff --git a/src/remote/activitypub/act/undo/follow.ts b/src/remote/activitypub/act/undo/follow.ts new file mode 100644 index 0000000000..fcf27c9507 --- /dev/null +++ b/src/remote/activitypub/act/undo/follow.ts @@ -0,0 +1,26 @@ +import parseAcct from '../../../../acct/parse'; +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 prefix = config.url + '/@'; + const id = typeof activity == 'string' ? activity : activity.id; + + if (!id.startsWith(prefix)) { + return null; + } + + const { username, host } = parseAcct(id.slice(prefix.length)); + if (host !== null) { + throw new Error(); + } + + const followee = await User.findOne({ username, host }); + if (followee === 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 index aa60d3a4fa..3ede9fcfb8 100644 --- a/src/remote/activitypub/act/undo/index.ts +++ b/src/remote/activitypub/act/undo/index.ts @@ -1,27 +1,37 @@ -import act from '../../act'; -import deleteObject from '../../delete'; -import unfollow from './unfollow'; +import * as debug from 'debug'; + +import { IRemoteUser } from '../../../../models/user'; +import { IUndo } from '../../type'; +import unfollow from './follow'; import Resolver from '../../resolver'; -export default async (resolver: Resolver, actor, activity): Promise<void> => { +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => { if ('actor' in activity && actor.account.uri !== activity.actor) { - throw new Error(); + throw new Error('invalid actor'); } - const results = await act(resolver, actor, activity.object); + const uri = activity.id || activity; + + log(`Undo: ${uri}`); - await Promise.all(results.map(async promisedResult => { - const result = await promisedResult; + const resolver = new Resolver(); - if (result === null || await deleteObject(result) !== null) { - return; - } + let object; - switch (result.object.$ref) { - case 'following': - await unfollow(result.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/act/undo/unfollow.ts b/src/remote/activitypub/act/undo/unfollow.ts deleted file mode 100644 index 4f15d9a3e4..0000000000 --- a/src/remote/activitypub/act/undo/unfollow.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createHttp } from '../../../../queue'; - -export default ({ $id }) => new Promise((resolve, reject) => { - createHttp({ type: 'unfollow', id: $id }).save(error => { - if (error) { - reject(error); - } else { - resolve(); - } - }); -}); diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts deleted file mode 100644 index bbe595a454..0000000000 --- a/src/remote/activitypub/create.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { JSDOM } from 'jsdom'; -import { ObjectID } from 'mongodb'; -import parseAcct from '../../acct/parse'; -import config from '../../config'; -import DriveFile from '../../models/drive-file'; -import Post from '../../models/post'; -import User from '../../models/user'; -import { IRemoteUser } from '../../models/user'; -import uploadFromUrl from '../../drive/upload-from-url'; -import createPost from '../../post/create'; -import distributePost from '../../post/distribute'; -import resolvePerson from './resolve-person'; -import Resolver from './resolver'; -const createDOMPurify = require('dompurify'); - -type IResult = { - resolver: Resolver; - object: { - $ref: string; - $id: ObjectID; - }; -}; - -class Creator { - private actor: IRemoteUser; - private distribute: boolean; - - constructor(actor, distribute) { - this.actor = actor; - this.distribute = distribute; - } - - private async createImage(resolver: Resolver, image) { - if ('attributedTo' in image && this.actor.account.uri !== image.attributedTo) { - throw new Error(); - } - - const { _id } = await uploadFromUrl(image.url, this.actor, image.id || null); - return { - resolver, - object: { $ref: 'driveFiles.files', $id: _id } - }; - } - - private async createNote(resolver: Resolver, note) { - if ( - ('attributedTo' in note && this.actor.account.uri !== note.attributedTo) || - typeof note.id !== 'string' - ) { - throw new Error(); - } - - const { window } = new JSDOM(note.content); - const mentions = []; - const tags = []; - - for (const { href, name, type } of note.tags) { - switch (type) { - case 'Hashtag': - if (name.startsWith('#')) { - tags.push(name.slice(1)); - } - break; - - case 'Mention': - mentions.push(resolvePerson(resolver, href)); - break; - } - } - - const [mediaIds, reply] = await Promise.all([ - 'attachment' in note && this.create(resolver, note.attachment) - .then(collection => Promise.all(collection)) - .then(collection => collection - .filter(media => media !== null && media.object.$ref === 'driveFiles.files') - .map(({ object }: IResult) => object.$id)), - - 'inReplyTo' in note && this.create(resolver, note.inReplyTo) - .then(collection => Promise.all(collection.map(promise => promise.then(result => { - if (result !== null && result.object.$ref === 'posts') { - throw result.object; - } - }, () => { })))) - .then(() => null, ({ $id }) => Post.findOne({ _id: $id })) - ]); - - const inserted = await createPost({ - channelId: undefined, - index: undefined, - createdAt: new Date(note.published), - mediaIds, - poll: undefined, - text: window.document.body.textContent, - textHtml: note.content && createDOMPurify(window).sanitize(note.content), - userId: this.actor._id, - appId: null, - viaMobile: false, - geo: undefined, - uri: note.id, - tags - }, reply, null, await Promise.all(mentions)); - - const promises = []; - - if (this.distribute) { - promises.push(distributePost(this.actor, inserted.mentions, inserted)); - } - - // Register to search database - if (note.content && config.elasticsearch.enable) { - const es = require('../../db/elasticsearch'); - - promises.push(new Promise((resolve, reject) => { - es.index({ - index: 'misskey', - type: 'post', - id: inserted._id.toString(), - body: { - text: window.document.body.textContent - } - }, resolve); - })); - } - - await Promise.all(promises); - - return { - resolver, - object: { $ref: 'posts', id: inserted._id } - }; - } - - public async create(parentResolver: Resolver, value): Promise<Array<Promise<IResult>>> { - const collection = await parentResolver.resolveCollection(value); - - return collection.object.map(async element => { - const uri = element.id || element; - const localPrefix = config.url + '/@'; - - if (uri.startsWith(localPrefix)) { - const [acct, id] = uri.slice(localPrefix).split('/', 2); - const user = await User.aggregate([ - { - $match: parseAcct(acct) - }, - { - $lookup: { - from: 'posts', - localField: '_id', - foreignField: 'userId', - as: 'post' - } - }, - { - $match: { - post: { _id: id } - } - } - ]); - - if (user === null || user.posts.length <= 0) { - throw new Error(); - } - - return { - resolver: collection.resolver, - object: { - $ref: 'posts', - id - } - }; - } - - try { - await Promise.all([ - DriveFile.findOne({ 'metadata.uri': uri }).then(file => { - if (file === null) { - return; - } - - throw { - $ref: 'driveFile.files', - $id: file._id - }; - }, () => {}), - Post.findOne({ uri }).then(post => { - if (post === null) { - return; - } - - throw { - $ref: 'posts', - $id: post._id - }; - }, () => {}) - ]); - } catch (object) { - return { - resolver: collection.resolver, - object - }; - } - - const { resolver, object } = await collection.resolver.resolveOne(element); - - switch (object.type) { - case 'Image': - return this.createImage(resolver, object); - - case 'Note': - return this.createNote(resolver, object); - } - - return null; - }); - } -} - -export default (resolver: Resolver, actor, value, distribute?: boolean) => { - const creator = new Creator(actor, distribute); - return creator.create(resolver, value); -}; diff --git a/src/remote/activitypub/delete/index.ts b/src/remote/activitypub/delete/index.ts deleted file mode 100644 index bc9104284b..0000000000 --- a/src/remote/activitypub/delete/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import deletePost from './post'; - -export default async ({ object }) => { - switch (object.$ref) { - case 'posts': - return deletePost(object); - } - - return null; -}; diff --git a/src/remote/activitypub/delete/post.ts b/src/remote/activitypub/delete/post.ts deleted file mode 100644 index 59ae8c2b94..0000000000 --- a/src/remote/activitypub/delete/post.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Post from '../../../models/post'; -import { createDb } from '../../../queue'; - -export default async ({ $id }) => { - const promisedDeletion = Post.findOneAndDelete({ _id: $id }); - - await new Promise((resolve, reject) => createDb({ - type: 'deletePostDependents', - id: $id - }).delay(65536).save(error => error ? reject(error) : resolve())); - - return promisedDeletion; -}; diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index 43531b121a..b971a53951 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -2,11 +2,14 @@ import renderDocument from './document'; import renderHashtag from './hashtag'; import config from '../../../config'; import DriveFile from '../../../models/drive-file'; -import Post from '../../../models/post'; -import User from '../../../models/user'; +import Post, { IPost } from '../../../models/post'; +import User, { IUser } from '../../../models/user'; + +export default async (user: IUser, post: IPost) => { + const promisedFiles = post.mediaIds + ? DriveFile.find({ _id: { $in: post.mediaIds } }) + : Promise.resolve([]); -export default async (user, post) => { - const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } }); let inReplyTo; if (post.replyId) { @@ -16,11 +19,11 @@ export default async (user, post) => { if (inReplyToPost !== null) { const inReplyToUser = await User.findOne({ - _id: post.userId, + _id: inReplyToPost.userId, }); if (inReplyToUser !== null) { - inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`; + inReplyTo = inReplyToPost.uri || `${config.url}/@${inReplyToUser.username}/${inReplyToPost._id}`; } } } else { @@ -39,6 +42,6 @@ export default async (user, post) => { cc: `${attributedTo}/followers`, inReplyTo, attachment: (await promisedFiles).map(renderDocument), - tag: post.tags.map(renderHashtag) + tag: (post.tags || []).map(renderHashtag) }; }; diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts index 84746169f5..b3bac3cd3f 100644 --- a/src/remote/activitypub/resolve-person.ts +++ b/src/remote/activitypub/resolve-person.ts @@ -3,15 +3,12 @@ import { toUnicode } from 'punycode'; import parseAcct from '../../acct/parse'; import config from '../../config'; import User, { validateUsername, isValidName, isValidDescription } from '../../models/user'; -import { createHttp } from '../../queue'; import webFinger from '../webfinger'; -import create from './create'; +import Resolver from './resolver'; +import uploadFromUrl from '../../services/drive/upload-from-url'; +import { isCollectionOrOrderedCollection } from './type'; -async function isCollection(collection) { - return ['Collection', 'OrderedCollection'].includes(collection.type); -} - -export default async (parentResolver, value, verifier?: string) => { +export default async (value, verifier?: string) => { const id = value.id || value; const localPrefix = config.url + '/@'; @@ -19,34 +16,35 @@ export default async (parentResolver, value, verifier?: string) => { return User.findOne(parseAcct(id.slice(localPrefix))); } - const { resolver, object } = await parentResolver.resolveOne(value); + const resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; if ( - object === null || - object.id !== id || + object == null || object.type !== 'Person' || typeof object.preferredUsername !== 'string' || !validateUsername(object.preferredUsername) || - !isValidName(object.name) || + !isValidName(object.name == '' ? null : object.name) || !isValidDescription(object.summary) ) { - throw new Error(); + throw new Error('invalid person'); } - const [followers, following, outbox, finger] = await Promise.all([ - resolver.resolveOne(object.followers).then( - resolved => isCollection(resolved.object) ? resolved.object : null, - () => null + const [followersCount = 0, followingCount = 0, postsCount = 0, finger] = await Promise.all([ + resolver.resolve(object.followers).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined ), - resolver.resolveOne(object.following).then( - resolved => isCollection(resolved.object) ? resolved.object : null, - () => null + resolver.resolve(object.following).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined ), - resolver.resolveOne(object.outbox).then( - resolved => isCollection(resolved.object) ? resolved.object : null, - () => null + resolver.resolve(object.outbox).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined ), - webFinger(id, verifier), + webFinger(id, verifier) ]); const host = toUnicode(finger.subject.replace(/^.*?@/, '')); @@ -57,12 +55,12 @@ export default async (parentResolver, value, verifier?: string) => { const user = await User.insert({ avatarId: null, bannerId: null, - createdAt: Date.parse(object.published), + createdAt: Date.parse(object.published) || null, description: summaryDOM.textContent, - followersCount: followers ? followers.totalItem || 0 : 0, - followingCount: following ? following.totalItem || 0 : 0, + followersCount, + followingCount, + postsCount, name: object.name, - postsCount: outbox ? outbox.totalItem || 0 : 0, driveCapacity: 1024 * 1024 * 8, // 8MiB username: object.preferredUsername, usernameLower: object.preferredUsername.toLowerCase(), @@ -78,34 +76,14 @@ export default async (parentResolver, value, verifier?: string) => { }, }); - createHttp({ - type: 'performActivityPub', - actor: user._id, - outbox - }).save(); - - const [avatarId, bannerId] = await Promise.all([ + const [avatarId, bannerId] = (await Promise.all([ object.icon, object.image - ].map(async value => { - if (value === undefined) { - return null; - } - - try { - const created = await create(resolver, user, value); - - await Promise.all(created.map(asyncCreated => asyncCreated.then(created => { - if (created !== null && created.object.$ref === 'driveFiles.files') { - throw created.object.$id; - } - }, () => {}))); - - return null; - } catch (id) { - return id; - } - })); + ].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 } }); diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 371ccdcc30..4a97e2ef66 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -1,20 +1,51 @@ -const request = require('request-promise-native'); +import * as request from 'request-promise-native'; +import * as debug from 'debug'; +import { IObject } from './type'; + +const log = debug('misskey:activitypub:resolver'); export default class Resolver { - private requesting: Set<string>; + private history: Set<string>; - constructor(iterable?: Iterable<string>) { - this.requesting = new Set(iterable); + constructor() { + this.history = new Set(); } - private async resolveUnrequestedOne(value) { + 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 { resolver: this, object: value }; + return value; } - const resolver = new Resolver(this.requesting); + if (this.history.has(value)) { + throw new Error('cannot resolve already resolved one'); + } - resolver.requesting.add(value); + this.history.add(value); const object = await request({ url: value, @@ -29,41 +60,11 @@ export default class Resolver { !object['@context'].includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' )) { - throw new Error(); + throw new Error('invalid response'); } - return { resolver, object }; - } - - public async resolveCollection(value) { - const resolved = typeof value === 'string' ? - await this.resolveUnrequestedOne(value) : - { resolver: this, object: value }; - - switch (resolved.object.type) { - case 'Collection': - resolved.object = resolved.object.items; - break; - - case 'OrderedCollection': - resolved.object = resolved.object.orderedItems; - break; - - default: - if (!Array.isArray(value)) { - resolved.object = [resolved.object]; - } - break; - } - - return resolved; - } - - public resolveOne(value) { - if (this.requesting.has(value)) { - throw new Error(); - } + log(`resolved: ${JSON.stringify(object, null, 2)}`); - return this.resolveUnrequestedOne(value); + return object; } } diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 94e2c350a2..9a4b3c75fc 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -1,3 +1,48 @@ -export type IObject = { +export type Object = { [x: string]: any }; + +export interface IObject { + '@context': string | object | any[]; type: string; -}; + id?: string; + summary?: 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 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 IUndo extends IActivity { + type: 'Undo'; +} + +export interface IFollow extends IActivity { + type: 'Follow'; +} diff --git a/src/remote/request.ts b/src/remote/request.ts index 72262cbf61..a375aebfbb 100644 --- a/src/remote/request.ts +++ b/src/remote/request.ts @@ -1,9 +1,15 @@ import { request } from 'https'; import { sign } from 'http-signature'; import { URL } from 'url'; +import * as debug from 'debug'; + import config from '../config'; +const log = debug('misskey:activitypub:deliver'); + export default ({ account, username }, url, object) => new Promise((resolve, reject) => { + log(`--> ${url}`); + const { protocol, hostname, port, pathname, search } = new URL(url); const req = request({ @@ -14,6 +20,8 @@ export default ({ account, username }, url, object) => new Promise((resolve, rej path: pathname + search, }, res => { res.on('end', () => { + log(`${url} --> ${res.statusCode}`); + if (res.statusCode >= 200 && res.statusCode < 300) { resolve(); } else { diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts index 097ed66738..9e1ae51952 100644 --- a/src/remote/resolve-user.ts +++ b/src/remote/resolve-user.ts @@ -1,7 +1,6 @@ import { toUnicode, toASCII } from 'punycode'; import User from '../models/user'; import resolvePerson from './activitypub/resolve-person'; -import Resolver from './activitypub/resolver'; import webFinger from './webfinger'; export default async (username, host, option) => { @@ -17,10 +16,10 @@ export default async (username, host, option) => { const finger = await webFinger(acctLower, acctLower); const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); if (!self) { - throw new Error(); + throw new Error('self link not found'); } - user = await resolvePerson(new Resolver(), self.href, acctLower); + user = await resolvePerson(self.href, acctLower); } return user; |