From cd2542e0fd8578f6e41114ffebbda1f16f7d04ce Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 2 Apr 2018 04:15:27 +0900 Subject: Refactor --- src/remote/activitypub/act/create.ts | 9 ++ src/remote/activitypub/act/index.ts | 22 +++++ src/remote/activitypub/create.ts | 87 ++++++++++++++++ src/remote/activitypub/renderer/context.ts | 5 + src/remote/activitypub/renderer/document.ts | 7 ++ src/remote/activitypub/renderer/follow.ts | 8 ++ src/remote/activitypub/renderer/hashtag.ts | 7 ++ src/remote/activitypub/renderer/image.ts | 6 ++ src/remote/activitypub/renderer/key.ts | 10 ++ src/remote/activitypub/renderer/note.ts | 44 +++++++++ .../activitypub/renderer/ordered-collection.ts | 6 ++ src/remote/activitypub/renderer/person.ts | 20 ++++ src/remote/activitypub/resolve-person.ts | 109 +++++++++++++++++++++ src/remote/activitypub/resolver.ts | 97 ++++++++++++++++++ src/remote/activitypub/type.ts | 3 + src/remote/resolve-user.ts | 26 +++++ src/remote/webfinger.ts | 25 +++++ 17 files changed, 491 insertions(+) create mode 100644 src/remote/activitypub/act/create.ts create mode 100644 src/remote/activitypub/act/index.ts create mode 100644 src/remote/activitypub/create.ts create mode 100644 src/remote/activitypub/renderer/context.ts create mode 100644 src/remote/activitypub/renderer/document.ts create mode 100644 src/remote/activitypub/renderer/follow.ts create mode 100644 src/remote/activitypub/renderer/hashtag.ts create mode 100644 src/remote/activitypub/renderer/image.ts create mode 100644 src/remote/activitypub/renderer/key.ts create mode 100644 src/remote/activitypub/renderer/note.ts create mode 100644 src/remote/activitypub/renderer/ordered-collection.ts create mode 100644 src/remote/activitypub/renderer/person.ts create mode 100644 src/remote/activitypub/resolve-person.ts create mode 100644 src/remote/activitypub/resolver.ts create mode 100644 src/remote/activitypub/type.ts create mode 100644 src/remote/resolve-user.ts create mode 100644 src/remote/webfinger.ts (limited to 'src/remote') diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts new file mode 100644 index 0000000000..9eb74800ea --- /dev/null +++ b/src/remote/activitypub/act/create.ts @@ -0,0 +1,9 @@ +import create from '../create'; + +export default (resolver, actor, activity) => { + if ('actor' in activity && actor.account.uri !== activity.actor) { + throw new Error(); + } + + return create(resolver, actor, activity.object); +}; diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts new file mode 100644 index 0000000000..a76983638f --- /dev/null +++ b/src/remote/activitypub/act/index.ts @@ -0,0 +1,22 @@ +import create from './create'; +import createObject from '../create'; +import Resolver from '../resolver'; + +export default (actor, value) => { + return new Resolver().resolve(value).then(resolved => Promise.all(resolved.map(async promisedResult => { + const { resolver, object } = await promisedResult; + const created = await (await createObject(resolver, actor, [object]))[0]; + + if (created !== null) { + return created; + } + + switch (object.type) { + case 'Create': + return create(resolver, actor, object); + + default: + return null; + } + }))); +}; diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts new file mode 100644 index 0000000000..e610232f51 --- /dev/null +++ b/src/remote/activitypub/create.ts @@ -0,0 +1,87 @@ +import { JSDOM } from 'jsdom'; +import config from '../../conf'; +import Post from '../../models/post'; +import RemoteUserObject, { IRemoteUserObject } from '../../models/remote-user-object'; +import uploadFromUrl from '../../common/drive/upload-from-url'; +import Resolver from './resolver'; +const createDOMPurify = require('dompurify'); + +function createRemoteUserObject($ref, $id, { id }) { + const object = { $ref, $id }; + + if (!id) { + return { object }; + } + + return RemoteUserObject.insert({ uri: id, object }); +} + +async function createImage(actor, object) { + if ('attributedTo' in object && actor.account.uri !== object.attributedTo) { + throw new Error(); + } + + const { _id } = await uploadFromUrl(object.url, actor); + return createRemoteUserObject('driveFiles.files', _id, object); +} + +async function createNote(resolver, actor, object) { + if ('attributedTo' in object && actor.account.uri !== object.attributedTo) { + throw new Error(); + } + + const mediaIds = 'attachment' in object && + (await Promise.all(await create(resolver, actor, object.attachment))) + .filter(media => media !== null && media.object.$ref === 'driveFiles.files') + .map(({ object }) => object.$id); + + const { window } = new JSDOM(object.content); + + const { _id } = await Post.insert({ + channelId: undefined, + index: undefined, + createdAt: new Date(object.published), + mediaIds, + replyId: undefined, + repostId: undefined, + poll: undefined, + text: window.document.body.textContent, + textHtml: object.content && createDOMPurify(window).sanitize(object.content), + userId: actor._id, + appId: null, + viaMobile: false, + geo: undefined + }); + + // Register to search database + if (object.content && config.elasticsearch.enable) { + const es = require('../../db/elasticsearch'); + + es.index({ + index: 'misskey', + type: 'post', + id: _id.toString(), + body: { + text: window.document.body.textContent + } + }); + } + + return createRemoteUserObject('posts', _id, object); +} + +export default async function create(parentResolver: Resolver, actor, value): Promise>> { + const results = await parentResolver.resolveRemoteUserObjects(value); + + return results.map(promisedResult => promisedResult.then(({ resolver, object }) => { + switch (object.type) { + case 'Image': + return createImage(actor, object); + + case 'Note': + return createNote(resolver, actor, object); + } + + return null; + })); +} 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/document.ts b/src/remote/activitypub/renderer/document.ts new file mode 100644 index 0000000000..fdd52c1b6c --- /dev/null +++ b/src/remote/activitypub/renderer/document.ts @@ -0,0 +1,7 @@ +import config from '../../../conf'; + +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..c99bc375a2 --- /dev/null +++ b/src/remote/activitypub/renderer/follow.ts @@ -0,0 +1,8 @@ +import config from '../../../conf'; +import { IRemoteUser } from '../../../models/user'; + +export default ({ username }, followee: IRemoteUser) => ({ + type: 'Follow', + actor: `${config.url}/@${username}`, + object: followee.account.uri +}); diff --git a/src/remote/activitypub/renderer/hashtag.ts b/src/remote/activitypub/renderer/hashtag.ts new file mode 100644 index 0000000000..c2d261ed21 --- /dev/null +++ b/src/remote/activitypub/renderer/hashtag.ts @@ -0,0 +1,7 @@ +import config from '../../../conf'; + +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..3d1c71cb95 --- /dev/null +++ b/src/remote/activitypub/renderer/image.ts @@ -0,0 +1,6 @@ +import config from '../../../conf'; + +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..904a69e081 --- /dev/null +++ b/src/remote/activitypub/renderer/key.ts @@ -0,0 +1,10 @@ +import config from '../../../conf'; +import { extractPublic } from '../../../crypto_key'; +import { ILocalUser } from '../../../models/user'; + +export default (user: ILocalUser) => ({ + id: `${config.url}/@${user.username}/publickey`, + type: 'Key', + owner: `${config.url}/@${user.username}`, + publicKeyPem: extractPublic(user.account.keypair) +}); diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts new file mode 100644 index 0000000000..74806f14b4 --- /dev/null +++ b/src/remote/activitypub/renderer/note.ts @@ -0,0 +1,44 @@ +import renderDocument from './document'; +import renderHashtag from './hashtag'; +import config from '../../../conf'; +import DriveFile from '../../../models/drive-file'; +import Post from '../../../models/post'; +import User from '../../../models/user'; + +export default async (user, post) => { + const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } }); + let inReplyTo; + + if (post.replyId) { + const inReplyToPost = await Post.findOne({ + _id: post.replyId, + }); + + if (inReplyToPost !== null) { + const inReplyToUser = await User.findOne({ + _id: post.userId, + }); + + if (inReplyToUser !== null) { + inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`; + } + } + } else { + inReplyTo = null; + } + + const attributedTo = `${config.url}/@${user.username}`; + + return { + id: `${attributedTo}/${post._id}`, + type: 'Note', + attributedTo, + content: post.textHtml, + published: post.createdAt.toISOString(), + to: 'https://www.w3.org/ns/activitystreams#Public', + cc: `${attributedTo}/followers`, + inReplyTo, + attachment: (await promisedFiles).map(renderDocument), + tag: post.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..c6c7893165 --- /dev/null +++ b/src/remote/activitypub/renderer/person.ts @@ -0,0 +1,20 @@ +import renderImage from './image'; +import renderKey from './key'; +import config from '../../../conf'; + +export default user => { + const id = `${config.url}/@${user.username}`; + + return { + type: 'Person', + id, + inbox: `${id}/inbox`, + outbox: `${id}/outbox`, + 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/resolve-person.ts b/src/remote/activitypub/resolve-person.ts new file mode 100644 index 0000000000..d928e7ce19 --- /dev/null +++ b/src/remote/activitypub/resolve-person.ts @@ -0,0 +1,109 @@ +import { JSDOM } from 'jsdom'; +import { toUnicode } from 'punycode'; +import User, { validateUsername, isValidName, isValidDescription } from '../../models/user'; +import queue from '../../queue'; +import webFinger from '../webfinger'; +import create from './create'; +import Resolver from './resolver'; + +async function isCollection(collection) { + return ['Collection', 'OrderedCollection'].includes(collection.type); +} + +export default async (value, usernameLower, hostLower, acctLower) => { + if (!validateUsername(usernameLower)) { + throw new Error(); + } + + const { resolver, object } = await new Resolver().resolveOne(value); + + if ( + object === null || + object.type !== 'Person' || + typeof object.preferredUsername !== 'string' || + object.preferredUsername.toLowerCase() !== usernameLower || + !isValidName(object.name) || + !isValidDescription(object.summary) + ) { + throw new Error(); + } + + const [followers, following, outbox, finger] = await Promise.all([ + resolver.resolveOne(object.followers).then( + resolved => isCollection(resolved.object) ? resolved.object : null, + () => null + ), + resolver.resolveOne(object.following).then( + resolved => isCollection(resolved.object) ? resolved.object : null, + () => null + ), + resolver.resolveOne(object.outbox).then( + resolved => isCollection(resolved.object) ? resolved.object : null, + () => null + ), + webFinger(object.id, acctLower), + ]); + + const summaryDOM = JSDOM.fragment(object.summary); + + // Create user + const user = await User.insert({ + avatarId: null, + bannerId: null, + createdAt: Date.parse(object.published), + description: summaryDOM.textContent, + followersCount: followers.totalItem, + followingCount: following.totalItem, + name: object.name, + postsCount: outbox.totalItem, + driveCapacity: 1024 * 1024 * 8, // 8MiB + username: object.preferredUsername, + usernameLower, + host: toUnicode(finger.subject.replace(/^.*?@/, '')), + hostLower, + account: { + publicKey: { + id: object.publicKey.id, + publicKeyPem: object.publicKey.publicKeyPem + }, + inbox: object.inbox, + uri: object.id, + }, + }); + + queue.create('http', { + type: 'performActivityPub', + actor: user._id, + outbox + }).save(); + + 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; + } + })); + + User.update({ _id: user._id }, { $set: { avatarId, bannerId } }); + + user.avatarId = avatarId; + user.bannerId = bannerId; + + return user; +}; diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts new file mode 100644 index 0000000000..ebfe25fe7e --- /dev/null +++ b/src/remote/activitypub/resolver.ts @@ -0,0 +1,97 @@ +import RemoteUserObject from '../../models/remote-user-object'; +import { IObject } from './type'; +const request = require('request-promise-native'); + +type IResult = { + resolver: Resolver; + object: IObject; +}; + +export default class Resolver { + private requesting: Set; + + constructor(iterable?: Iterable) { + this.requesting = new Set(iterable); + } + + private async resolveUnrequestedOne(value) { + if (typeof value !== 'string') { + return { resolver: this, object: value }; + } + + const resolver = new Resolver(this.requesting); + + resolver.requesting.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' + )) { + throw new Error(); + } + + return { resolver, object }; + } + + private async resolveCollection(value) { + if (Array.isArray(value)) { + return value; + } + + const resolved = typeof value === 'string' ? + await this.resolveUnrequestedOne(value) : + value; + + switch (resolved.type) { + case 'Collection': + return resolved.items; + + case 'OrderedCollection': + return resolved.orderedItems; + + default: + return [resolved]; + } + } + + public async resolve(value): Promise>> { + const collection = await this.resolveCollection(value); + + return collection + .filter(element => !this.requesting.has(element)) + .map(this.resolveUnrequestedOne.bind(this)); + } + + public resolveOne(value) { + if (this.requesting.has(value)) { + throw new Error(); + } + + return this.resolveUnrequestedOne(value); + } + + public async resolveRemoteUserObjects(value) { + const collection = await this.resolveCollection(value); + + return collection.filter(element => !this.requesting.has(element)).map(element => { + if (typeof element === 'string') { + const object = RemoteUserObject.findOne({ uri: element }); + + if (object !== null) { + return object; + } + } + + return this.resolveUnrequestedOne(element); + }); + } +} diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts new file mode 100644 index 0000000000..94e2c350a2 --- /dev/null +++ b/src/remote/activitypub/type.ts @@ -0,0 +1,3 @@ +export type IObject = { + type: string; +}; diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts new file mode 100644 index 0000000000..a393092839 --- /dev/null +++ b/src/remote/resolve-user.ts @@ -0,0 +1,26 @@ +import { toUnicode, toASCII } from 'punycode'; +import User from '../models/user'; +import resolvePerson from './activitypub/resolve-person'; +import webFinger from './webfinger'; + +export default async (username, host, option) => { + const usernameLower = username.toLowerCase(); + const hostLowerAscii = toASCII(host).toLowerCase(); + const hostLower = toUnicode(hostLowerAscii); + + let user = await User.findOne({ usernameLower, hostLower }, option); + + if (user === null) { + const acctLower = `${usernameLower}@${hostLowerAscii}`; + + const finger = await webFinger(acctLower, acctLower); + const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); + if (!self) { + throw new Error(); + } + + user = await resolvePerson(self.href, usernameLower, hostLower, acctLower); + } + + return user; +}; diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts new file mode 100644 index 0000000000..fec5da689c --- /dev/null +++ b/src/remote/webfinger.ts @@ -0,0 +1,25 @@ +const WebFinger = require('webfinger.js'); + +const webFinger = new WebFinger({ }); + +type ILink = { + href: string; + rel: string; +}; + +type IWebFinger = { + links: ILink[]; + subject: string; +}; + +export default (query, verifier): Promise => new Promise((res, rej) => webFinger.lookup(query, (error, result) => { + if (error) { + return rej(error); + } + + if (result.object.subject.toLowerCase().replace(/^acct:/, '') !== verifier) { + return rej('WebFinger verfification failed'); + } + + res(result.object); +})); -- cgit v1.2.3-freya From 5b9f3701f58ca00c151498d16b6a839a91ba8643 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Mon, 2 Apr 2018 12:58:53 +0900 Subject: Abolish common and misc directories --- src/client/app/ch/tags/channel.tag | 2 +- .../app/common/scripts/compose-notification.ts | 4 +- .../app/common/views/components/autocomplete.vue | 2 +- .../views/components/messaging-room.message.vue | 4 +- .../app/common/views/components/messaging.vue | 2 +- .../app/common/views/components/othello.game.vue | 2 +- .../app/common/views/components/othello.room.vue | 2 +- .../app/common/views/components/post-html.ts | 4 +- .../common/views/components/welcome-timeline.vue | 2 +- .../app/desktop/views/components/friends-maker.vue | 2 +- .../views/components/messaging-room-window.vue | 2 +- .../app/desktop/views/components/notifications.vue | 4 +- .../desktop/views/components/post-detail.sub.vue | 2 +- .../app/desktop/views/components/post-detail.vue | 4 +- .../app/desktop/views/components/post-preview.vue | 2 +- .../desktop/views/components/posts.post.sub.vue | 2 +- .../app/desktop/views/components/posts.post.vue | 4 +- .../app/desktop/views/components/settings.mute.vue | 2 +- .../app/desktop/views/components/user-preview.vue | 4 +- .../desktop/views/components/users-list.item.vue | 2 +- src/client/app/desktop/views/pages/home.vue | 2 +- .../app/desktop/views/pages/messaging-room.vue | 2 +- .../views/pages/user/user.followers-you-know.vue | 2 +- .../app/desktop/views/pages/user/user.friends.vue | 2 +- .../app/desktop/views/pages/user/user.header.vue | 2 +- src/client/app/desktop/views/pages/user/user.vue | 2 +- src/client/app/desktop/views/pages/welcome.vue | 2 +- .../desktop/views/widgets/channel.channel.post.vue | 2 +- src/client/app/desktop/views/widgets/polls.vue | 2 +- src/client/app/desktop/views/widgets/trends.vue | 2 +- src/client/app/desktop/views/widgets/users.vue | 2 +- src/client/app/mobile/api/post.ts | 2 +- .../views/components/notification-preview.vue | 2 +- .../app/mobile/views/components/notification.vue | 4 +- .../app/mobile/views/components/post-card.vue | 4 +- .../mobile/views/components/post-detail.sub.vue | 2 +- .../app/mobile/views/components/post-detail.vue | 4 +- .../app/mobile/views/components/post-preview.vue | 2 +- .../app/mobile/views/components/post.sub.vue | 2 +- src/client/app/mobile/views/components/post.vue | 4 +- .../app/mobile/views/components/user-card.vue | 2 +- .../app/mobile/views/components/user-preview.vue | 2 +- src/client/app/mobile/views/pages/followers.vue | 2 +- src/client/app/mobile/views/pages/following.vue | 2 +- src/client/app/mobile/views/pages/home.vue | 2 +- .../app/mobile/views/pages/messaging-room.vue | 2 +- src/client/app/mobile/views/pages/messaging.vue | 2 +- src/client/app/mobile/views/pages/user.vue | 4 +- .../views/pages/user/home.followers-you-know.vue | 2 +- .../app/mobile/views/pages/user/home.photos.vue | 2 +- src/common/drive/add-file.ts | 307 ------- src/common/drive/upload-from-url.ts | 46 -- src/common/event.ts | 80 -- src/common/notify.ts | 50 -- src/common/push-sw.ts | 52 -- src/common/text/html.ts | 83 -- src/common/text/parse/core/syntax-highlighter.ts | 334 -------- src/common/text/parse/elements/bold.ts | 14 - src/common/text/parse/elements/code.ts | 17 - src/common/text/parse/elements/emoji.ts | 14 - src/common/text/parse/elements/hashtag.ts | 19 - src/common/text/parse/elements/inline-code.ts | 17 - src/common/text/parse/elements/link.ts | 19 - src/common/text/parse/elements/mention.ts | 17 - src/common/text/parse/elements/quote.ts | 14 - src/common/text/parse/elements/url.ts | 14 - src/common/text/parse/index.ts | 72 -- src/drive/add-file.ts | 307 +++++++ src/drive/upload-from-url.ts | 46 ++ src/event.ts | 80 ++ src/get-notification-summary.ts | 27 + src/get-post-summary.ts | 45 + src/get-reaction-emoji.ts | 14 + src/misc/get-notification-summary.ts | 27 - src/misc/get-post-summary.ts | 45 - src/misc/get-reaction-emoji.ts | 14 - src/misc/othello/ai/back.ts | 376 --------- src/misc/othello/ai/front.ts | 233 ------ src/misc/othello/ai/index.ts | 1 - src/misc/othello/core.ts | 340 -------- src/misc/othello/maps.ts | 911 --------------------- src/misc/user/get-acct.ts | 3 - src/misc/user/get-summary.ts | 18 - src/misc/user/parse-acct.ts | 4 - src/notify.ts | 50 ++ src/othello/ai/back.ts | 376 +++++++++ src/othello/ai/front.ts | 233 ++++++ src/othello/ai/index.ts | 1 + src/othello/core.ts | 340 ++++++++ src/othello/maps.ts | 911 +++++++++++++++++++++ src/processor/http/follow.ts | 4 +- src/push-sw.ts | 52 ++ src/remote/activitypub/create.ts | 2 +- src/server/activitypub/inbox.ts | 2 +- src/server/activitypub/post.ts | 2 +- src/server/activitypub/with-user.ts | 2 +- src/server/api/bot/core.ts | 8 +- src/server/api/bot/interfaces/line.ts | 6 +- src/server/api/common/read-messaging-message.ts | 6 +- src/server/api/common/read-notification.ts | 2 +- src/server/api/endpoints/drive/files/create.ts | 2 +- src/server/api/endpoints/drive/files/update.ts | 2 +- .../api/endpoints/drive/files/upload_from_url.ts | 2 +- src/server/api/endpoints/drive/folders/create.ts | 2 +- src/server/api/endpoints/drive/folders/update.ts | 2 +- src/server/api/endpoints/following/delete.ts | 2 +- src/server/api/endpoints/i/regenerate_token.ts | 2 +- src/server/api/endpoints/i/update.ts | 2 +- .../api/endpoints/i/update_client_setting.ts | 2 +- src/server/api/endpoints/i/update_home.ts | 2 +- src/server/api/endpoints/i/update_mobile_home.ts | 2 +- .../api/endpoints/messaging/messages/create.ts | 8 +- .../endpoints/notifications/mark_as_read_all.ts | 2 +- src/server/api/endpoints/othello/games/show.ts | 2 +- src/server/api/endpoints/othello/match.ts | 4 +- src/server/api/endpoints/posts/create.ts | 12 +- src/server/api/endpoints/posts/polls/vote.ts | 4 +- src/server/api/endpoints/posts/reactions/create.ts | 4 +- src/server/api/limitter.ts | 2 +- src/server/api/private/signin.ts | 2 +- src/server/api/service/twitter.ts | 2 +- src/server/api/stream/othello-game.ts | 6 +- src/server/api/stream/othello.ts | 2 +- src/server/webfinger.ts | 2 +- src/text/html.ts | 83 ++ src/text/parse/core/syntax-highlighter.ts | 334 ++++++++ src/text/parse/elements/bold.ts | 14 + src/text/parse/elements/code.ts | 17 + src/text/parse/elements/emoji.ts | 14 + src/text/parse/elements/hashtag.ts | 19 + src/text/parse/elements/inline-code.ts | 17 + src/text/parse/elements/link.ts | 19 + src/text/parse/elements/mention.ts | 17 + src/text/parse/elements/quote.ts | 14 + src/text/parse/elements/url.ts | 14 + src/text/parse/index.ts | 72 ++ src/user/get-acct.ts | 3 + src/user/get-summary.ts | 18 + src/user/parse-acct.ts | 4 + 139 files changed, 3257 insertions(+), 3257 deletions(-) delete mode 100644 src/common/drive/add-file.ts delete mode 100644 src/common/drive/upload-from-url.ts delete mode 100644 src/common/event.ts delete mode 100644 src/common/notify.ts delete mode 100644 src/common/push-sw.ts delete mode 100644 src/common/text/html.ts delete mode 100644 src/common/text/parse/core/syntax-highlighter.ts delete mode 100644 src/common/text/parse/elements/bold.ts delete mode 100644 src/common/text/parse/elements/code.ts delete mode 100644 src/common/text/parse/elements/emoji.ts delete mode 100644 src/common/text/parse/elements/hashtag.ts delete mode 100644 src/common/text/parse/elements/inline-code.ts delete mode 100644 src/common/text/parse/elements/link.ts delete mode 100644 src/common/text/parse/elements/mention.ts delete mode 100644 src/common/text/parse/elements/quote.ts delete mode 100644 src/common/text/parse/elements/url.ts delete mode 100644 src/common/text/parse/index.ts create mode 100644 src/drive/add-file.ts create mode 100644 src/drive/upload-from-url.ts create mode 100644 src/event.ts create mode 100644 src/get-notification-summary.ts create mode 100644 src/get-post-summary.ts create mode 100644 src/get-reaction-emoji.ts delete mode 100644 src/misc/get-notification-summary.ts delete mode 100644 src/misc/get-post-summary.ts delete mode 100644 src/misc/get-reaction-emoji.ts delete mode 100644 src/misc/othello/ai/back.ts delete mode 100644 src/misc/othello/ai/front.ts delete mode 100644 src/misc/othello/ai/index.ts delete mode 100644 src/misc/othello/core.ts delete mode 100644 src/misc/othello/maps.ts delete mode 100644 src/misc/user/get-acct.ts delete mode 100644 src/misc/user/get-summary.ts delete mode 100644 src/misc/user/parse-acct.ts create mode 100644 src/notify.ts create mode 100644 src/othello/ai/back.ts create mode 100644 src/othello/ai/front.ts create mode 100644 src/othello/ai/index.ts create mode 100644 src/othello/core.ts create mode 100644 src/othello/maps.ts create mode 100644 src/push-sw.ts create mode 100644 src/text/html.ts create mode 100644 src/text/parse/core/syntax-highlighter.ts create mode 100644 src/text/parse/elements/bold.ts create mode 100644 src/text/parse/elements/code.ts create mode 100644 src/text/parse/elements/emoji.ts create mode 100644 src/text/parse/elements/hashtag.ts create mode 100644 src/text/parse/elements/inline-code.ts create mode 100644 src/text/parse/elements/link.ts create mode 100644 src/text/parse/elements/mention.ts create mode 100644 src/text/parse/elements/quote.ts create mode 100644 src/text/parse/elements/url.ts create mode 100644 src/text/parse/index.ts create mode 100644 src/user/get-acct.ts create mode 100644 src/user/get-summary.ts create mode 100644 src/user/parse-acct.ts (limited to 'src/remote') diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag index 70e494aedd..0c139ba267 100644 --- a/src/client/app/ch/tags/channel.tag +++ b/src/client/app/ch/tags/channel.tag @@ -229,7 +229,7 @@ - +
- { post.index }: - { getUserName(post.user) } - - + { note.index }: + { getUserName(note.user) } + + ID:{ acct }
- >>{ post.reply.index } - { post.text } -
- @@ -26,7 +26,7 @@ export default Vue.extend({ fetching: true, moreFetching: false, existMore: false, - posts: [], + notes: [], connection: null, connectionId: null, date: null @@ -41,7 +41,7 @@ export default Vue.extend({ this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); - this.connection.on('post', this.onPost); + this.connection.on('note', this.onNote); this.connection.on('follow', this.onChangeFollowing); this.connection.on('unfollow', this.onChangeFollowing); @@ -51,7 +51,7 @@ export default Vue.extend({ this.fetch(); }, beforeDestroy() { - this.connection.off('post', this.onPost); + this.connection.off('note', this.onNote); this.connection.off('follow', this.onChangeFollowing); this.connection.off('unfollow', this.onChangeFollowing); (this as any).os.stream.dispose(this.connectionId); @@ -63,45 +63,45 @@ export default Vue.extend({ fetch(cb?) { this.fetching = true; - (this as any).api('posts/timeline', { + (this as any).api('notes/timeline', { limit: 11, untilDate: this.date ? this.date.getTime() : undefined - }).then(posts => { - if (posts.length == 11) { - posts.pop(); + }).then(notes => { + if (notes.length == 11) { + notes.pop(); this.existMore = true; } - this.posts = posts; + this.notes = notes; this.fetching = false; this.$emit('loaded'); if (cb) cb(); }); }, more() { - if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return; + if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return; this.moreFetching = true; - (this as any).api('posts/timeline', { + (this as any).api('notes/timeline', { limit: 11, - untilId: this.posts[this.posts.length - 1].id - }).then(posts => { - if (posts.length == 11) { - posts.pop(); + untilId: this.notes[this.notes.length - 1].id + }).then(notes => { + if (notes.length == 11) { + notes.pop(); } else { this.existMore = false; } - this.posts = this.posts.concat(posts); + this.notes = this.notes.concat(notes); this.moreFetching = false; }); }, - onPost(post) { + onNote(note) { // サウンドを再生する if ((this as any).os.isEnableSounds) { - const sound = new Audio(`${url}/assets/post.mp3`); + const sound = new Audio(`${url}/assets/note.mp3`); sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; sound.play(); } - this.posts.unshift(post); + this.notes.unshift(note); }, onChangeFollowing() { this.fetch(); diff --git a/src/client/app/desktop/views/components/ui.header.post.vue b/src/client/app/desktop/views/components/ui.header.post.vue index c2f0e07dd3..5c1756b756 100644 --- a/src/client/app/desktop/views/components/ui.header.post.vue +++ b/src/client/app/desktop/views/components/ui.header.post.vue @@ -1,6 +1,6 @@ @@ -19,7 +19,7 @@ export default Vue.extend({ diff --git a/src/client/app/desktop/views/pages/post.vue b/src/client/app/desktop/views/pages/post.vue deleted file mode 100644 index dbd707e049..0000000000 --- a/src/client/app/desktop/views/pages/post.vue +++ /dev/null @@ -1,67 +0,0 @@ - - - - - diff --git a/src/client/app/desktop/views/pages/search.vue b/src/client/app/desktop/views/pages/search.vue index afd37c8cee..698154e667 100644 --- a/src/client/app/desktop/views/pages/search.vue +++ b/src/client/app/desktop/views/pages/search.vue @@ -7,12 +7,12 @@

%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。

- +
-
+ @@ -30,7 +30,7 @@ export default Vue.extend({ moreFetching: false, existMore: false, offset: 0, - posts: [] + notes: [] }; }, watch: { @@ -38,7 +38,7 @@ export default Vue.extend({ }, computed: { empty(): boolean { - return this.posts.length == 0; + return this.notes.length == 0; }, q(): string { return this.$route.query.q; @@ -66,33 +66,33 @@ export default Vue.extend({ this.fetching = true; Progress.start(); - (this as any).api('posts/search', Object.assign({ + (this as any).api('notes/search', Object.assign({ limit: limit + 1, offset: this.offset - }, parse(this.q))).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); + }, parse(this.q))).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); this.existMore = true; } - this.posts = posts; + this.notes = notes; this.fetching = false; Progress.done(); }); }, more() { - if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return; + if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return; this.offset += limit; this.moreFetching = true; - return (this as any).api('posts/search', Object.assign({ + return (this as any).api('notes/search', Object.assign({ limit: limit + 1, offset: this.offset - }, parse(this.q))).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); + }, parse(this.q))).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); } else { this.existMore = false; } - this.posts = this.posts.concat(posts); + this.notes = this.notes.concat(notes); this.moreFetching = false; }); }, @@ -111,7 +111,7 @@ export default Vue.extend({ margin 0 auto color #555 -.posts +.notes max-width 600px margin 0 auto border solid 1px rgba(0, 0, 0, 0.075) diff --git a/src/client/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue index 071c9bb61c..ed3b1c7104 100644 --- a/src/client/app/desktop/views/pages/user/user.home.vue +++ b/src/client/app/desktop/views/pages/user/user.home.vue @@ -9,7 +9,7 @@
- +
diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue index 1ff79b4aee..99a1a8d707 100644 --- a/src/client/app/desktop/views/pages/user/user.photos.vue +++ b/src/client/app/desktop/views/pages/user/user.photos.vue @@ -22,13 +22,13 @@ export default Vue.extend({ }; }, mounted() { - (this as any).api('users/posts', { + (this as any).api('users/notes', { userId: this.user.id, withMedia: true, limit: 9 - }).then(posts => { - posts.forEach(post => { - post.media.forEach(media => { + }).then(notes => { + notes.forEach(note => { + note.media.forEach(media => { if (this.images.length < 9) this.images.push(media); }); }); diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue index f5562d0915..2a82ba7861 100644 --- a/src/client/app/desktop/views/pages/user/user.profile.vue +++ b/src/client/app/desktop/views/pages/user/user.profile.vue @@ -14,7 +14,7 @@

%fa:B twitter%@{{ user.account.twitter.screenName }}

-

%fa:angle-right%{{ user.postsCount }}投稿

+

%fa:angle-right%{{ user.notesCount }}投稿

%fa:angle-right%{{ user.followingCount }}人をフォロー

%fa:angle-right%{{ user.followersCount }}人のフォロワー

diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue index 134ad423ce..87d133174b 100644 --- a/src/client/app/desktop/views/pages/user/user.timeline.vue +++ b/src/client/app/desktop/views/pages/user/user.timeline.vue @@ -8,12 +8,12 @@

%fa:R comments%このユーザーはまだ何も投稿していないようです。

- +
-
+ @@ -27,7 +27,7 @@ export default Vue.extend({ moreFetching: false, mode: 'default', unreadCount: 0, - posts: [], + notes: [], date: null }; }, @@ -38,7 +38,7 @@ export default Vue.extend({ }, computed: { empty(): boolean { - return this.posts.length == 0; + return this.notes.length == 0; } }, mounted() { @@ -60,26 +60,26 @@ export default Vue.extend({ } }, fetch(cb?) { - (this as any).api('users/posts', { + (this as any).api('users/notes', { userId: this.user.id, untilDate: this.date ? this.date.getTime() : undefined, with_replies: this.mode == 'with-replies' - }).then(posts => { - this.posts = posts; + }).then(notes => { + this.notes = notes; this.fetching = false; if (cb) cb(); }); }, more() { - if (this.moreFetching || this.fetching || this.posts.length == 0) return; + if (this.moreFetching || this.fetching || this.notes.length == 0) return; this.moreFetching = true; - (this as any).api('users/posts', { + (this as any).api('users/notes', { userId: this.user.id, with_replies: this.mode == 'with-replies', - untilId: this.posts[this.posts.length - 1].id - }).then(posts => { + untilId: this.notes[this.notes.length - 1].id + }).then(notes => { this.moreFetching = false; - this.posts = this.posts.concat(posts); + this.notes = this.notes.concat(notes); }); }, onScroll() { diff --git a/src/client/app/desktop/views/widgets/channel.channel.form.vue b/src/client/app/desktop/views/widgets/channel.channel.form.vue index aaf327f1ef..f2744268bb 100644 --- a/src/client/app/desktop/views/widgets/channel.channel.form.vue +++ b/src/client/app/desktop/views/widgets/channel.channel.form.vue @@ -24,11 +24,11 @@ export default Vue.extend({ if (/^>>([0-9]+) /.test(this.text)) { const index = this.text.match(/^>>([0-9]+) /)[1]; - reply = (this.$parent as any).posts.find(p => p.index.toString() == index); + reply = (this.$parent as any).notes.find(p => p.index.toString() == index); this.text = this.text.replace(/^>>([0-9]+) /, ''); } - (this as any).api('posts/create', { + (this as any).api('notes/create', { text: this.text, replyId: reply ? reply.id : undefined, channelId: (this.$parent as any).channel.id diff --git a/src/client/app/desktop/views/widgets/channel.channel.note.vue b/src/client/app/desktop/views/widgets/channel.channel.note.vue new file mode 100644 index 0000000000..313a2e3f4f --- /dev/null +++ b/src/client/app/desktop/views/widgets/channel.channel.note.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue deleted file mode 100644 index fa6d8c34a5..0000000000 --- a/src/client/app/desktop/views/widgets/channel.channel.post.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - - - diff --git a/src/client/app/desktop/views/widgets/channel.channel.vue b/src/client/app/desktop/views/widgets/channel.channel.vue index e9fb9e3fd7..ea4d8f8454 100644 --- a/src/client/app/desktop/views/widgets/channel.channel.vue +++ b/src/client/app/desktop/views/widgets/channel.channel.vue @@ -1,9 +1,9 @@ - + @@ -36,7 +36,7 @@ export default define({ post() { this.posting = true; - (this as any).api('posts/create', { + (this as any).api('notes/create', { text: this.text }).then(data => { this.clear(); diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue index 27c1860b32..c2c7636bb3 100644 --- a/src/client/app/desktop/views/widgets/trends.vue +++ b/src/client/app/desktop/views/widgets/trends.vue @@ -5,8 +5,8 @@

%fa:spinner .pulse .fw%%i18n:common.loading%

-
-

{{ post.text }}

+
+

{{ note.text }}

@{{ acct }}

%i18n:desktop.tags.mk-trends-home-widget.nothing%

@@ -25,12 +25,12 @@ export default define({ }).extend({ computed: { acct() { - return getAcct(this.post.user); + return getAcct(this.note.user); }, }, data() { return { - post: null, + note: null, fetching: true, offset: 0 }; @@ -44,23 +44,23 @@ export default define({ }, fetch() { this.fetching = true; - this.post = null; + this.note = null; - (this as any).api('posts/trend', { + (this as any).api('notes/trend', { limit: 1, offset: this.offset, - repost: false, + renote: false, reply: false, media: false, poll: false - }).then(posts => { - const post = posts ? posts[0] : null; - if (post == null) { + }).then(notes => { + const note = notes ? notes[0] : null; + if (note == null) { this.offset = 0; } else { this.offset++; } - this.post = post; + this.note = note; this.fetching = false; }); } @@ -103,7 +103,7 @@ export default define({ &:active color #999 - > .post + > .note padding 16px font-size 12px font-style oblique diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue index e407ca00d7..c9d5971395 100644 --- a/src/client/app/dev/views/new-app.vue +++ b/src/client/app/dev/views/new-app.vue @@ -27,7 +27,7 @@ アカウントの情報を見る。 アカウントの情報を操作する。 - 投稿する。 + 投稿する。 リアクションしたりリアクションをキャンセルする。 フォローしたりフォロー解除する。 ドライブを見る。 diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts index 98309ba8de..72919c6505 100644 --- a/src/client/app/mobile/api/post.ts +++ b/src/client/app/mobile/api/post.ts @@ -1,24 +1,24 @@ import PostForm from '../views/components/post-form.vue'; -//import RepostForm from '../views/components/repost-form.vue'; -import getPostSummary from '../../../../renderers/get-post-summary'; +//import RenoteForm from '../views/components/renote-form.vue'; +import getNoteSummary from '../../../../renderers/get-note-summary'; export default (os) => (opts) => { const o = opts || {}; - if (o.repost) { - /*const vm = new RepostForm({ + if (o.renote) { + /*const vm = new RenoteForm({ propsData: { - repost: o.repost + renote: o.renote } }).$mount(); vm.$once('cancel', recover); - vm.$once('post', recover); + vm.$once('note', recover); document.body.appendChild(vm.$el);*/ - const text = window.prompt(`「${getPostSummary(o.repost)}」をRepost`); + const text = window.prompt(`「${getNoteSummary(o.renote)}」をRenote`); if (text == null) return; - os.api('posts/create', { - repostId: o.repost.id, + os.api('notes/create', { + renoteId: o.renote.id, text: text == '' ? undefined : text }); } else { @@ -36,7 +36,7 @@ export default (os) => (opts) => { } }).$mount(); vm.$once('cancel', recover); - vm.$once('post', recover); + vm.$once('note', recover); document.body.appendChild(vm.$el); (vm as any).focus(); } diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index 4776fccddb..6265d0d45f 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -25,7 +25,7 @@ import MkDrive from './views/pages/drive.vue'; import MkNotifications from './views/pages/notifications.vue'; import MkMessaging from './views/pages/messaging.vue'; import MkMessagingRoom from './views/pages/messaging-room.vue'; -import MkPost from './views/pages/post.vue'; +import MkNote from './views/pages/note.vue'; import MkSearch from './views/pages/search.vue'; import MkFollowers from './views/pages/followers.vue'; import MkFollowing from './views/pages/following.vue'; @@ -68,7 +68,7 @@ init((launch) => { { path: '/@:user', component: MkUser }, { path: '/@:user/followers', component: MkFollowers }, { path: '/@:user/following', component: MkFollowing }, - { path: '/@:user/:post', component: MkPost } + { path: '/@:user/:note', component: MkNote } ] }); diff --git a/src/client/app/mobile/views/components/activity.vue b/src/client/app/mobile/views/components/activity.vue index 2e44017e77..dcd319cb69 100644 --- a/src/client/app/mobile/views/components/activity.vue +++ b/src/client/app/mobile/views/components/activity.vue @@ -2,14 +2,14 @@
- - @@ -32,12 +32,12 @@ export default Vue.extend({ userId: this.user.id, limit: 30 }).then(data => { - data.forEach(d => d.total = d.posts + d.replies + d.reposts); + data.forEach(d => d.total = d.notes + d.replies + d.renotes); this.peak = Math.max.apply(null, data.map(d => d.total)); data.forEach(d => { - d.postsH = d.posts / this.peak; + d.notesH = d.notes / this.peak; d.repliesH = d.replies / this.peak; - d.repostsH = d.reposts / this.peak; + d.renotesH = d.renotes / this.peak; }); data.reverse(); this.data = data; diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts index fb8f65f47d..9346700304 100644 --- a/src/client/app/mobile/views/components/index.ts +++ b/src/client/app/mobile/views/components/index.ts @@ -2,16 +2,16 @@ import Vue from 'vue'; import ui from './ui.vue'; import timeline from './timeline.vue'; -import post from './post.vue'; -import posts from './posts.vue'; +import note from './note.vue'; +import notes from './notes.vue'; import mediaImage from './media-image.vue'; import mediaVideo from './media-video.vue'; import drive from './drive.vue'; -import postPreview from './post-preview.vue'; -import subPostContent from './sub-post-content.vue'; -import postCard from './post-card.vue'; +import notePreview from './note-preview.vue'; +import subNoteContent from './sub-note-content.vue'; +import noteCard from './note-card.vue'; import userCard from './user-card.vue'; -import postDetail from './post-detail.vue'; +import noteDetail from './note-detail.vue'; import followButton from './follow-button.vue'; import friendsMaker from './friends-maker.vue'; import notification from './notification.vue'; @@ -25,16 +25,16 @@ import widgetContainer from './widget-container.vue'; Vue.component('mk-ui', ui); Vue.component('mk-timeline', timeline); -Vue.component('mk-post', post); -Vue.component('mk-posts', posts); +Vue.component('mk-note', note); +Vue.component('mk-notes', notes); Vue.component('mk-media-image', mediaImage); Vue.component('mk-media-video', mediaVideo); Vue.component('mk-drive', drive); -Vue.component('mk-post-preview', postPreview); -Vue.component('mk-sub-post-content', subPostContent); -Vue.component('mk-post-card', postCard); +Vue.component('mk-note-preview', notePreview); +Vue.component('mk-sub-note-content', subNoteContent); +Vue.component('mk-note-card', noteCard); Vue.component('mk-user-card', userCard); -Vue.component('mk-post-detail', postDetail); +Vue.component('mk-note-detail', noteDetail); Vue.component('mk-follow-button', followButton); Vue.component('mk-friends-maker', friendsMaker); Vue.component('mk-notification', notification); diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue new file mode 100644 index 0000000000..9ad0d3e294 --- /dev/null +++ b/src/client/app/mobile/views/components/note-card.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/src/client/app/mobile/views/components/note-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue new file mode 100644 index 0000000000..38aea4ba20 --- /dev/null +++ b/src/client/app/mobile/views/components/note-detail.sub.vue @@ -0,0 +1,113 @@ + + + + + + diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue new file mode 100644 index 0000000000..e1682e58ed --- /dev/null +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -0,0 +1,462 @@ + + + + + + + diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue new file mode 100644 index 0000000000..8c8d8645bb --- /dev/null +++ b/src/client/app/mobile/views/components/note-preview.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue new file mode 100644 index 0000000000..a37d0dea08 --- /dev/null +++ b/src/client/app/mobile/views/components/note.sub.vue @@ -0,0 +1,119 @@ + + + + + + diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue new file mode 100644 index 0000000000..4b33c6f071 --- /dev/null +++ b/src/client/app/mobile/views/components/note.vue @@ -0,0 +1,540 @@ + + + + + + + diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue new file mode 100644 index 0000000000..573026d53e --- /dev/null +++ b/src/client/app/mobile/views/components/notes.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue index 0492c5d86c..79ca3321e4 100644 --- a/src/client/app/mobile/views/components/notification-preview.vue +++ b/src/client/app/mobile/views/components/notification-preview.vue @@ -4,23 +4,23 @@ avatar

{{ name }}

-

%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%

+

%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%

- @@ -51,7 +51,7 @@ avatar

%fa:chart-pie%{{ name }}

-

%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%

+

%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%

@@ -59,7 +59,7 @@ - - diff --git a/src/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue deleted file mode 100644 index 98d6a14cac..0000000000 --- a/src/client/app/mobile/views/components/post-detail.sub.vue +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue index 0226ce081a..e1682e58ed 100644 --- a/src/client/app/mobile/views/components/post-detail.vue +++ b/src/client/app/mobile/views/components/post-detail.vue @@ -1,5 +1,5 @@
- +
- +
-
+

- avatar + avatar %fa:retweet% {{ name }} - がRepost + がRenote

@@ -38,33 +38,33 @@
- +
{{ tag }}
- + %fa:map-marker-alt% 位置情報
-
- +
+
- - - -
- +
@@ -84,9 +84,9 @@ import getAcct from '../../../../../acct/render'; import getUserName from '../../../../../renderers/get-user-name'; import parse from '../../../../../text/parse'; -import MkPostMenu from '../../../common/views/components/post-menu.vue'; +import MkNoteMenu from '../../../common/views/components/note-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; -import XSub from './post-detail.sub.vue'; +import XSub from './note-detail.sub.vue'; export default Vue.extend({ components: { @@ -94,7 +94,7 @@ export default Vue.extend({ }, props: { - post: { + note: { type: Object, required: true }, @@ -113,10 +113,10 @@ export default Vue.extend({ computed: { acct(): string { - return getAcct(this.post.user); + return getAcct(this.note.user); }, name(): string { - return getUserName(this.post.user); + return getUserName(this.note.user); }, pAcct(): string { return getAcct(this.p.user); @@ -124,14 +124,14 @@ export default Vue.extend({ pName(): string { return getUserName(this.p.user); }, - isRepost(): boolean { - return (this.post.repost && - this.post.text == null && - this.post.mediaIds == null && - this.post.poll == null); + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.mediaIds == null && + this.note.poll == null); }, p(): any { - return this.isRepost ? this.post.repost : this.post; + return this.isRenote ? this.note.renote : this.note; }, reactionsCount(): number { return this.p.reactionCounts @@ -155,8 +155,8 @@ export default Vue.extend({ mounted() { // Get replies if (!this.compact) { - (this as any).api('posts/replies', { - postId: this.p.id, + (this as any).api('notes/replies', { + noteId: this.p.id, limit: 8 }).then(replies => { this.replies = replies; @@ -187,8 +187,8 @@ export default Vue.extend({ this.contextFetching = true; // Fetch context - (this as any).api('posts/context', { - postId: this.p.replyId + (this as any).api('notes/context', { + noteId: this.p.replyId }).then(context => { this.contextFetching = false; this.context = context.reverse(); @@ -199,22 +199,22 @@ export default Vue.extend({ reply: this.p }); }, - repost() { + renote() { (this as any).apis.post({ - repost: this.p + renote: this.p }); }, react() { (this as any).os.new(MkReactionPicker, { source: this.$refs.reactButton, - post: this.p, + note: this.p, compact: true }); }, menu() { - (this as any).os.new(MkPostMenu, { + (this as any).os.new(MkNoteMenu, { source: this.$refs.menuButton, - post: this.p, + note: this.p, compact: true }); } @@ -225,7 +225,7 @@ export default Vue.extend({ diff --git a/src/client/app/mobile/views/components/post.sub.vue b/src/client/app/mobile/views/components/post.sub.vue deleted file mode 100644 index 909d5cb597..0000000000 --- a/src/client/app/mobile/views/components/post.sub.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue index eee1e80fd3..4b33c6f071 100644 --- a/src/client/app/mobile/views/components/post.vue +++ b/src/client/app/mobile/views/components/post.vue @@ -1,19 +1,19 @@ @@ -25,7 +25,7 @@ export default Vue.extend({ return { fetching: true, existMore: false, - posts: [], + notes: [], offset: 0 }; }, @@ -48,30 +48,30 @@ export default Vue.extend({ this.fetching = true; Progress.start(); - (this as any).api('posts/search', Object.assign({ + (this as any).api('notes/search', Object.assign({ limit: limit + 1 - }, parse(this.q))).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); + }, parse(this.q))).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); this.existMore = true; } - this.posts = posts; + this.notes = notes; this.fetching = false; Progress.done(); }); }, more() { this.offset += limit; - (this as any).api('posts/search', Object.assign({ + (this as any).api('notes/search', Object.assign({ limit: limit + 1, offset: this.offset - }, parse(this.q))).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); + }, parse(this.q))).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); } else { this.existMore = false; } - this.posts = this.posts.concat(posts); + this.notes = this.notes.concat(notes); }); } } @@ -79,7 +79,7 @@ export default Vue.extend({ diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue index ecf5082072..1c59260812 100644 --- a/src/client/app/mobile/views/pages/user/home.photos.vue +++ b/src/client/app/mobile/views/pages/user/home.photos.vue @@ -5,7 +5,7 @@

%i18n:mobile.tags.mk-user-overview-photos.no-photos%

@@ -28,15 +28,15 @@ export default Vue.extend({ getAcct }, mounted() { - (this as any).api('users/posts', { + (this as any).api('users/notes', { userId: this.user.id, withMedia: true, limit: 6 - }).then(posts => { - posts.forEach(post => { - post.media.forEach(media => { + }).then(notes => { + notes.forEach(note => { + note.media.forEach(media => { if (this.images.length < 9) this.images.push({ - post, + note, media }); }); diff --git a/src/client/app/mobile/views/pages/user/home.posts.vue b/src/client/app/mobile/views/pages/user/home.posts.vue deleted file mode 100644 index 654f7f63e0..0000000000 --- a/src/client/app/mobile/views/pages/user/home.posts.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - - - diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue index 1afcd1f5ba..2554084969 100644 --- a/src/client/app/mobile/views/pages/user/home.vue +++ b/src/client/app/mobile/views/pages/user/home.vue @@ -1,10 +1,10 @@ diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue index 17cdf93065..27baf8bee4 100644 --- a/src/client/app/mobile/views/pages/welcome.vue +++ b/src/client/app/mobile/views/pages/welcome.vue @@ -8,7 +8,7 @@
- +
@@ -70,7 +70,7 @@ export default Vue.extend({ (this as any).api('signin', { username: this.username, password: this.password, - token: this.user && this.user.account.twoFactorEnabled ? this.token : undefined + token: this.user && this.user.twoFactorEnabled ? this.token : undefined }).then(() => { location.reload(); }).catch(() => { diff --git a/src/models/user.ts b/src/models/user.ts index f86aefe9a4..906bcb533b 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -11,7 +11,7 @@ import config from '../config'; const User = db.get('users'); User.createIndex('username'); -User.createIndex('account.token'); +User.createIndex('token'); export default User; @@ -40,45 +40,41 @@ type IUserBase = { export interface ILocalUser extends IUserBase { host: null; - account: { - keypair: string; - email: string; - links: string[]; - password: string; - token: string; - twitter: { - accessToken: string; - accessTokenSecret: string; - userId: string; - screenName: string; - }; - line: { - userId: string; - }; - profile: { - location: string; - birthday: string; // 'YYYY-MM-DD' - tags: string[]; - }; - lastUsedAt: Date; - isBot: boolean; - isPro: boolean; - twoFactorSecret: string; - twoFactorEnabled: boolean; - twoFactorTempSecret: string; - clientSettings: any; - settings: any; + keypair: string; + email: string; + links: string[]; + password: string; + token: string; + twitter: { + accessToken: string; + accessTokenSecret: string; + userId: string; + screenName: string; }; + line: { + userId: string; + }; + profile: { + location: string; + birthday: string; // 'YYYY-MM-DD' + tags: string[]; + }; + lastUsedAt: Date; + isBot: boolean; + isPro: boolean; + twoFactorSecret: string; + twoFactorEnabled: boolean; + twoFactorTempSecret: string; + clientSettings: any; + settings: any; } export interface IRemoteUser extends IUserBase { - account: { - inbox: string; - uri: string; - publicKey: { - id: string; - publicKeyPem: string; - }; + inbox: string; + uri: string; + publicKey: { + id: string; + publicKeyPem: string; }; } @@ -150,11 +146,11 @@ export const pack = ( const fields = opts.detail ? { } : { - 'account.settings': false, - 'account.clientSettings': false, - 'account.profile': false, - 'account.keywords': false, - 'account.domains': false + settings: false, + clientSettings: false, + profile: false, + keywords: false, + domains: false }; // Populate the user if 'user' is ID @@ -188,29 +184,29 @@ export const pack = ( // Remove needless properties delete _user.latestNote; - if (!_user.host) { + if (_user.host == null) { // Remove private properties - delete _user.account.keypair; - delete _user.account.password; - delete _user.account.token; - delete _user.account.twoFactorTempSecret; - delete _user.account.twoFactorSecret; + delete _user.keypair; + delete _user.password; + delete _user.token; + delete _user.twoFactorTempSecret; + delete _user.twoFactorSecret; delete _user.usernameLower; - if (_user.account.twitter) { - delete _user.account.twitter.accessToken; - delete _user.account.twitter.accessTokenSecret; + if (_user.twitter) { + delete _user.twitter.accessToken; + delete _user.twitter.accessTokenSecret; } - delete _user.account.line; + delete _user.line; // Visible via only the official client if (!opts.includeSecrets) { - delete _user.account.email; - delete _user.account.settings; - delete _user.account.clientSettings; + delete _user.email; + delete _user.settings; + delete _user.clientSettings; } if (!opts.detail) { - delete _user.account.twoFactorEnabled; + delete _user.twoFactorEnabled; } } diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index eb4b62d37f..6608907a7a 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -36,7 +36,7 @@ export default async (job: kue.Job, done): Promise => { } else { user = await User.findOne({ host: { $ne: null }, - 'account.publicKey.id': signature.keyId + 'publicKey.id': signature.keyId }) as IRemoteUser; // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する @@ -50,7 +50,7 @@ export default async (job: kue.Job, done): Promise => { return; } - if (!verifySignature(signature, user.account.publicKey.publicKeyPem)) { + if (!verifySignature(signature, user.publicKey.publicKeyPem)) { console.warn('signature verification failed'); done(); return; diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts index 30a75e7377..c87423c5fd 100644 --- a/src/remote/activitypub/act/create/image.ts +++ b/src/remote/activitypub/act/create/image.ts @@ -7,7 +7,7 @@ import { IDriveFile } from '../../../../models/drive-file'; const log = debug('misskey:activitypub'); export default async function(actor: IRemoteUser, image): Promise { - if ('attributedTo' in image && actor.account.uri !== image.attributedTo) { + if ('attributedTo' in image && actor.uri !== image.attributedTo) { log(`invalid image: ${JSON.stringify(image, null, 2)}`); throw new Error('invalid image'); } diff --git a/src/remote/activitypub/act/create/index.ts b/src/remote/activitypub/act/create/index.ts index dd0b112141..7cb9b08449 100644 --- a/src/remote/activitypub/act/create/index.ts +++ b/src/remote/activitypub/act/create/index.ts @@ -9,7 +9,7 @@ import { ICreate } from '../../type'; const log = debug('misskey:activitypub'); export default async (actor: IRemoteUser, activity: ICreate): Promise => { - if ('actor' in activity && actor.account.uri !== activity.actor) { + if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts index 6c6faa1ae5..10b47dc4ca 100644 --- a/src/remote/activitypub/act/delete/index.ts +++ b/src/remote/activitypub/act/delete/index.ts @@ -7,7 +7,7 @@ import { IRemoteUser } from '../../../../models/user'; * 削除アクティビティを捌きます */ export default async (actor: IRemoteUser, activity): Promise => { - if ('actor' in activity && actor.account.uri !== activity.actor) { + if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts index 3ede9fcfb8..71f547aeb9 100644 --- a/src/remote/activitypub/act/undo/index.ts +++ b/src/remote/activitypub/act/undo/index.ts @@ -8,7 +8,7 @@ import Resolver from '../../resolver'; const log = debug('misskey:activitypub'); export default async (actor: IRemoteUser, activity: IUndo): Promise => { - if ('actor' in activity && actor.account.uri !== activity.actor) { + if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts index 6d1ded9a95..0a1ae1a4b2 100644 --- a/src/remote/activitypub/renderer/follow.ts +++ b/src/remote/activitypub/renderer/follow.ts @@ -4,5 +4,5 @@ import { IRemoteUser } from '../../../models/user'; export default ({ username }, followee: IRemoteUser) => ({ type: 'Follow', actor: `${config.url}/@${username}`, - object: followee.account.uri + object: followee.uri }); diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts index 85be7b1367..76e2f13bcc 100644 --- a/src/remote/activitypub/renderer/key.ts +++ b/src/remote/activitypub/renderer/key.ts @@ -6,5 +6,5 @@ export default (user: ILocalUser) => ({ id: `${config.url}/@${user.username}/publickey`, type: 'Key', owner: `${config.url}/@${user.username}`, - publicKeyPem: extractPublic(user.account.keypair) + publicKeyPem: extractPublic(user.keypair) }); diff --git a/src/remote/request.ts b/src/remote/request.ts index a375aebfbb..a0c69cf4ef 100644 --- a/src/remote/request.ts +++ b/src/remote/request.ts @@ -4,10 +4,11 @@ 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 ({ account, username }, url, object) => new Promise((resolve, reject) => { +export default (user: ILocalUser, url, object) => new Promise((resolve, reject) => { log(`--> ${url}`); const { protocol, hostname, port, pathname, search } = new URL(url); @@ -35,8 +36,8 @@ export default ({ account, username }, url, object) => new Promise((resolve, rej sign(req, { authorizationHeaderName: 'Signature', - key: account.keypair, - keyId: `acct:${username}@${config.host}` + key: user.keypair, + keyId: `acct:${user.username}@${config.host}` }); req.end(JSON.stringify(object)); diff --git a/src/renderers/get-user-summary.ts b/src/renderers/get-user-summary.ts index 52309954d3..1bd9a7fb47 100644 --- a/src/renderers/get-user-summary.ts +++ b/src/renderers/get-user-summary.ts @@ -11,8 +11,7 @@ export default function(user: IUser): string { `${user.notesCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`; if (isLocalUser(user)) { - const account = user.account; - string += `場所: ${account.profile.location}、誕生日: ${account.profile.birthday}\n`; + string += `場所: ${user.profile.location}、誕生日: ${user.profile.birthday}\n`; } return string + `「${user.description}」`; diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts index 8566744831..adbeeb3b34 100644 --- a/src/server/api/authenticate.ts +++ b/src/server/api/authenticate.ts @@ -34,7 +34,7 @@ export default (req: express.Request) => new Promise(async (resolv if (isNativeToken(token)) { const user: IUser = await User - .findOne({ 'account.token': token }); + .findOne({ 'token': token }); if (user === null) { return reject('user not found'); diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts index 1cf0522349..d41af48057 100644 --- a/src/server/api/bot/core.ts +++ b/src/server/api/bot/core.ts @@ -226,7 +226,7 @@ class SigninContext extends Context { } } else { // Compare password - const same = await bcrypt.compare(query, this.temporaryUser.account.password); + const same = await bcrypt.compare(query, this.temporaryUser.password); if (same) { this.bot.signin(this.temporaryUser); diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts index b6b0c257e9..be3bfe33d3 100644 --- a/src/server/api/bot/interfaces/line.ts +++ b/src/server/api/bot/interfaces/line.ts @@ -112,11 +112,11 @@ class LineBot extends BotCore { data: `showtl|${user.id}` }); - if (user.account.twitter) { + if (user.twitter) { actions.push({ type: 'uri', label: 'Twitterアカウントを見る', - uri: `https://twitter.com/${user.account.twitter.screenName}` + uri: `https://twitter.com/${user.twitter.screenName}` }); } @@ -174,7 +174,7 @@ module.exports = async (app: express.Application) => { if (session == null) { const user = await User.findOne({ host: null, - 'account.line': { + 'line': { userId: sourceId } }); @@ -184,7 +184,7 @@ module.exports = async (app: express.Application) => { bot.on('signin', user => { User.update(user._id, { $set: { - 'account.line': { + 'line': { userId: sourceId } } @@ -194,7 +194,7 @@ module.exports = async (app: express.Application) => { bot.on('signout', user => { User.update(user._id, { $set: { - 'account.line': { + 'line': { userId: null } } diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts index f9688790c4..8bb327694d 100644 --- a/src/server/api/common/signin.ts +++ b/src/server/api/common/signin.ts @@ -2,7 +2,7 @@ import config from '../../../config'; export default function(res, user, redirect: boolean) { const expires = 1000 * 60 * 60 * 24 * 365; // One Year - res.cookie('i', user.account.token, { + res.cookie('i', user.token, { path: '/', domain: `.${config.hostname}`, secure: config.url.substr(0, 5) === 'https', diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index 0ccac8d83d..d3cc549ca7 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }, { fields: { data: false, - 'account.profile': false + 'profile': false } }); diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts index 0684b87504..0d0a6c7132 100644 --- a/src/server/api/endpoints/following/delete.ts +++ b/src/server/api/endpoints/following/delete.ts @@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }, { fields: { data: false, - 'account.profile': false + 'profile': false } }); diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts index 44de71d162..0be30500c4 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -5,12 +5,6 @@ import User, { pack } from '../../../models/user'; /** * Show myself - * - * @param {any} params - * @param {any} user - * @param {any} app - * @param {Boolean} isSecure - * @return {Promise} */ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => { // Serialize @@ -22,7 +16,7 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => // Update lastUsedAt User.update({ _id: user._id }, { $set: { - 'account.lastUsedAt': new Date() + lastUsedAt: new Date() } }); }); diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts index 0b2e32c13f..3e824feffd 100644 --- a/src/server/api/endpoints/i/2fa/done.ts +++ b/src/server/api/endpoints/i/2fa/done.ts @@ -28,8 +28,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { await User.update(user._id, { $set: { - 'account.twoFactorSecret': user.twoFactorTempSecret, - 'account.twoFactorEnabled': true + 'twoFactorSecret': user.twoFactorTempSecret, + 'twoFactorEnabled': true } }); diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts index dc7fb959bb..bed64a2545 100644 --- a/src/server/api/endpoints/i/2fa/register.ts +++ b/src/server/api/endpoints/i/2fa/register.ts @@ -14,7 +14,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (passwordErr) return rej('invalid password param'); // Compare password - const same = await bcrypt.compare(password, user.account.password); + const same = await bcrypt.compare(password, user.password); if (!same) { return rej('incorrect password'); diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts index ff2a435fee..f9d7a25f53 100644 --- a/src/server/api/endpoints/i/2fa/unregister.ts +++ b/src/server/api/endpoints/i/2fa/unregister.ts @@ -11,7 +11,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (passwordErr) return rej('invalid password param'); // Compare password - const same = await bcrypt.compare(password, user.account.password); + const same = await bcrypt.compare(password, user.password); if (!same) { return rej('incorrect password'); @@ -19,8 +19,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { await User.update(user._id, { $set: { - 'account.twoFactorSecret': null, - 'account.twoFactorEnabled': false + 'twoFactorSecret': null, + 'twoFactorEnabled': false } }); diff --git a/src/server/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts index a38b56a216..57415083f1 100644 --- a/src/server/api/endpoints/i/change_password.ts +++ b/src/server/api/endpoints/i/change_password.ts @@ -22,7 +22,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (newPasswordErr) return rej('invalid newPassword param'); // Compare password - const same = await bcrypt.compare(currentPassword, user.account.password); + const same = await bcrypt.compare(currentPassword, user.password); if (!same) { return rej('incorrect password'); @@ -34,7 +34,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { await User.update(user._id, { $set: { - 'account.password': hash + 'password': hash } }); diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts index 9aa6725f8c..f9e92c1797 100644 --- a/src/server/api/endpoints/i/regenerate_token.ts +++ b/src/server/api/endpoints/i/regenerate_token.ts @@ -20,7 +20,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (passwordErr) return rej('invalid password param'); // Compare password - const same = await bcrypt.compare(password, user.account.password); + const same = await bcrypt.compare(password, user.password); if (!same) { return rej('incorrect password'); @@ -31,7 +31,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { await User.update(user._id, { $set: { - 'account.token': secret + 'token': secret } }); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 279b062f52..a8caa0ebc4 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -29,12 +29,12 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re // Get 'location' parameter const [location, locationErr] = $(params.location).optional.nullable.string().pipe(isValidLocation).$; if (locationErr) return rej('invalid location param'); - if (location !== undefined) user.account.profile.location = location; + if (location !== undefined) user.profile.location = location; // Get 'birthday' parameter const [birthday, birthdayErr] = $(params.birthday).optional.nullable.string().pipe(isValidBirthday).$; if (birthdayErr) return rej('invalid birthday param'); - if (birthday !== undefined) user.account.profile.birthday = birthday; + if (birthday !== undefined) user.profile.birthday = birthday; // Get 'avatarId' parameter const [avatarId, avatarIdErr] = $(params.avatarId).optional.id().$; @@ -49,12 +49,12 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re // Get 'isBot' parameter const [isBot, isBotErr] = $(params.isBot).optional.boolean().$; if (isBotErr) return rej('invalid isBot param'); - if (isBot != null) user.account.isBot = isBot; + if (isBot != null) user.isBot = isBot; // Get 'autoWatch' parameter const [autoWatch, autoWatchErr] = $(params.autoWatch).optional.boolean().$; if (autoWatchErr) return rej('invalid autoWatch param'); - if (autoWatch != null) user.account.settings.autoWatch = autoWatch; + if (autoWatch != null) user.settings.autoWatch = autoWatch; await User.update(user._id, { $set: { @@ -62,9 +62,9 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re description: user.description, avatarId: user.avatarId, bannerId: user.bannerId, - 'account.profile': user.account.profile, - 'account.isBot': user.account.isBot, - 'account.settings': user.account.settings + 'profile': user.profile, + 'isBot': user.isBot, + 'settings': user.settings } }); diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts index 10741aceba..b0d5db5ec2 100644 --- a/src/server/api/endpoints/i/update_client_setting.ts +++ b/src/server/api/endpoints/i/update_client_setting.ts @@ -22,14 +22,14 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (valueErr) return rej('invalid value param'); const x = {}; - x[`account.clientSettings.${name}`] = value; + x[`clientSettings.${name}`] = value; await User.update(user._id, { $set: x }); // Serialize - user.account.clientSettings[name] = value; + user.clientSettings[name] = value; const iObj = await pack(user, user, { detail: true, includeSecrets: true diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts index 91be0714d7..ce7661ede0 100644 --- a/src/server/api/endpoints/i/update_home.ts +++ b/src/server/api/endpoints/i/update_home.ts @@ -26,7 +26,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (home) { await User.update(user._id, { $set: { - 'account.clientSettings.home': home + 'clientSettings.home': home } }); @@ -38,7 +38,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { } else { if (id == null && data == null) return rej('you need to set id and data params if home param unset'); - const _home = user.account.clientSettings.home; + const _home = user.clientSettings.home; const widget = _home.find(w => w.id == id); if (widget == null) return rej('widget not found'); @@ -47,7 +47,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { await User.update(user._id, { $set: { - 'account.clientSettings.home': _home + 'clientSettings.home': _home } }); diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts index 1efda120d5..b710e2f330 100644 --- a/src/server/api/endpoints/i/update_mobile_home.ts +++ b/src/server/api/endpoints/i/update_mobile_home.ts @@ -25,7 +25,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (home) { await User.update(user._id, { $set: { - 'account.clientSettings.mobileHome': home + 'clientSettings.mobileHome': home } }); @@ -37,7 +37,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { } else { if (id == null && data == null) return rej('you need to set id and data params if home param unset'); - const _home = user.account.clientSettings.mobileHome || []; + const _home = user.clientSettings.mobileHome || []; const widget = _home.find(w => w.id == id); if (widget == null) return rej('widget not found'); @@ -46,7 +46,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { await User.update(user._id, { $set: { - 'account.clientSettings.mobileHome': _home + 'clientSettings.mobileHome': _home } }); diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts index a7fa5f7b4b..19894d07af 100644 --- a/src/server/api/endpoints/mute/create.ts +++ b/src/server/api/endpoints/mute/create.ts @@ -30,7 +30,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }, { fields: { data: false, - 'account.profile': false + 'profile': false } }); diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts index 687f010336..10096352ba 100644 --- a/src/server/api/endpoints/mute/delete.ts +++ b/src/server/api/endpoints/mute/delete.ts @@ -30,7 +30,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }, { fields: { data: false, - 'account.profile': false + 'profile': false } }); diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts index 0e27f87ee2..fd4412ad35 100644 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -100,7 +100,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // この投稿をWatchする - if (user.account.settings.autoWatch !== false) { + if (user.settings.autoWatch !== false) { watch(user._id, note); } }); diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index 60483936fb..2de22da13e 100644 --- a/src/server/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -32,7 +32,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { }, $or: [ { - 'account.lastUsedAt': { + 'lastUsedAt': { $gte: new Date(Date.now() - ms('7days')) } }, { diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index e0bd67d1ca..d7c4832c95 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -37,7 +37,7 @@ export default async (req: express.Request, res: express.Response) => { }, { fields: { data: false, - 'account.profile': false + 'profile': false } }) as ILocalUser; @@ -48,15 +48,13 @@ export default async (req: express.Request, res: express.Response) => { return; } - const account = user.account; - // Compare password - const same = await bcrypt.compare(password, account.password); + const same = await bcrypt.compare(password, password); if (same) { - if (account.twoFactorEnabled) { + if (user.twoFactorEnabled) { const verified = (speakeasy as any).totp.verify({ - secret: account.twoFactorSecret, + secret: user.twoFactorSecret, encoding: 'base32', token: token }); diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index 5818ba25cd..f441e1b754 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -119,44 +119,29 @@ export default async (req: express.Request, res: express.Response) => { usernameLower: username.toLowerCase(), host: null, hostLower: null, - account: { - keypair: generateKeypair(), - token: secret, - email: null, - links: null, - password: hash, - profile: { - bio: null, - birthday: null, - blood: null, - gender: null, - handedness: null, - height: null, - location: null, - weight: null - }, - settings: { - autoWatch: true - }, - clientSettings: { - home: homeData - } + keypair: generateKeypair(), + token: secret, + email: null, + links: null, + password: hash, + profile: { + bio: null, + birthday: null, + blood: null, + gender: null, + handedness: null, + height: null, + location: null, + weight: null + }, + settings: { + autoWatch: true + }, + clientSettings: { + home: homeData } }); // Response res.send(await pack(account)); - - // Create search index - if (config.elasticsearch.enable) { - const es = require('../../db/elasticsearch'); - es.index({ - index: 'misskey', - type: 'user', - id: account._id.toString(), - body: { - username: username - } - }); - } }; diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts index 77b932b13b..da48e30a8a 100644 --- a/src/server/api/service/twitter.ts +++ b/src/server/api/service/twitter.ts @@ -40,10 +40,10 @@ module.exports = (app: express.Application) => { const user = await User.findOneAndUpdate({ host: null, - 'account.token': userToken + 'token': userToken }, { $set: { - 'account.twitter': null + 'twitter': null } }); @@ -128,7 +128,7 @@ module.exports = (app: express.Application) => { const user = await User.findOne({ host: null, - 'account.twitter.userId': result.userId + 'twitter.userId': result.userId }); if (user == null) { @@ -151,10 +151,10 @@ module.exports = (app: express.Application) => { const user = await User.findOneAndUpdate({ host: null, - 'account.token': userToken + 'token': userToken }, { $set: { - 'account.twitter': { + 'twitter': { accessToken: result.accessToken, accessTokenSecret: result.accessTokenSecret, userId: result.userId, diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts index 313558851b..359ef74aff 100644 --- a/src/server/api/stream/home.ts +++ b/src/server/api/stream/home.ts @@ -74,7 +74,7 @@ export default async function(request: websocket.request, connection: websocket. // Update lastUsedAt User.update({ _id: user._id }, { $set: { - 'account.lastUsedAt': new Date() + 'lastUsedAt': new Date() } }); break; diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index edcf505d24..26946b524e 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -97,7 +97,7 @@ function authenticate(token: string): Promise { const user: IUser = await User .findOne({ host: null, - 'account.token': token + 'token': token }); resolve(user); diff --git a/src/services/following/create.ts b/src/services/following/create.ts index d919f4487f..31e3be19ed 100644 --- a/src/services/following/create.ts +++ b/src/services/following/create.ts @@ -60,13 +60,13 @@ export default async function(follower: IUser, followee: IUser, activity?) { const content = renderFollow(follower, followee); content['@context'] = context; - deliver(follower, content, followee.account.inbox).save(); + deliver(follower, content, followee.inbox).save(); } if (isRemoteUser(follower) && isLocalUser(followee)) { const content = renderAccept(activity); content['@context'] = context; - deliver(followee, content, follower.account.inbox).save(); + deliver(followee, content, follower.inbox).save(); } } diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts index 364a4803b9..d79bf64f53 100644 --- a/src/services/following/delete.ts +++ b/src/services/following/delete.ts @@ -59,6 +59,6 @@ export default async function(follower: IUser, followee: IUser, activity?) { const content = renderUndo(renderFollow(follower, followee)); content['@context'] = context; - deliver(follower, content, followee.account.inbox).save(); + deliver(follower, content, followee.inbox).save(); } } diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 8eee8f44af..551d618569 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -78,7 +78,7 @@ export default async (user: IUser, data: { host: user.host, hostLower: user.hostLower, account: isLocalUser(user) ? {} : { - inbox: user.account.inbox + inbox: user.inbox } } }; @@ -133,7 +133,7 @@ export default async (user: IUser, data: { // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) { - deliver(user, content, data.reply._user.account.inbox).save(); + deliver(user, content, data.reply._user.inbox).save(); } Promise.all(followers.map(follower => { @@ -145,7 +145,7 @@ export default async (user: IUser, data: { } else { // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 if (isLocalUser(user)) { - deliver(user, content, follower.account.inbox).save(); + deliver(user, content, follower.inbox).save(); } } })); @@ -242,7 +242,7 @@ export default async (user: IUser, data: { }); // この投稿をWatchする - if (isLocalUser(user) && user.account.settings.autoWatch !== false) { + if (isLocalUser(user) && user.settings.autoWatch !== false) { watch(user._id, data.reply); } @@ -277,7 +277,7 @@ export default async (user: IUser, data: { }); // この投稿をWatchする - if (isLocalUser(user) && user.account.settings.autoWatch !== false) { + if (isLocalUser(user) && user.settings.autoWatch !== false) { watch(user._id, data.renote); } diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index d0ce65ee54..ea51b205d0 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -78,7 +78,7 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise }); // ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする - if (isLocalUser(user) && user.account.settings.autoWatch !== false) { + if (isLocalUser(user) && user.settings.autoWatch !== false) { watch(user._id, note); } @@ -88,7 +88,7 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 if (isLocalUser(user) && isRemoteUser(note._user)) { - deliver(user, content, note._user.account.inbox).save(); + deliver(user, content, note._user.inbox).save(); } //#endregion }); diff --git a/test/api.ts b/test/api.ts index 953c5aea0d..87bbb8ee16 100644 --- a/test/api.ts +++ b/test/api.ts @@ -32,7 +32,7 @@ const async = fn => (done) => { const request = (endpoint, params, me?) => new Promise((ok, ng) => { const auth = me ? { - i: me.account.token + i: me.token } : {}; _chai.request(server) @@ -157,10 +157,10 @@ describe('API', () => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('name').eql(myName); - res.body.should.have.nested.property('account.profile').a('object'); - res.body.should.have.nested.property('account.profile.location').eql(myLocation); - res.body.should.have.nested.property('account.profile.birthday').eql(myBirthday); - res.body.should.have.nested.property('account.profile.gender').eql('female'); + res.body.should.have.nested.property('profile').a('object'); + res.body.should.have.nested.property('profile.location').eql(myLocation); + res.body.should.have.nested.property('profile.birthday').eql(myBirthday); + res.body.should.have.nested.property('profile.gender').eql('female'); })); it('名前を空白にできない', async(async () => { @@ -180,8 +180,8 @@ describe('API', () => { }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.nested.property('account.profile').a('object'); - res.body.should.have.nested.property('account.profile.birthday').eql(null); + res.body.should.have.nested.property('profile').a('object'); + res.body.should.have.nested.property('profile.birthday').eql(null); })); it('不正な誕生日の形式で怒られる', async(async () => { @@ -736,7 +736,7 @@ describe('API', () => { const me = await insertSakurako(); const res = await _chai.request(server) .post('/drive/files/create') - .field('i', me.account.token) + .field('i', me.token) .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png'); res.should.have.status(200); res.body.should.be.a('object'); diff --git a/tools/migration/nighthike/12.js b/tools/migration/nighthike/12.js new file mode 100644 index 0000000000..f4b61e2ee8 --- /dev/null +++ b/tools/migration/nighthike/12.js @@ -0,0 +1,58 @@ +// for Node.js interpret + +const { default: User } = require('../../../built/models/user'); +const { generate } = require('../../../built/crypto_key'); +const { default: zip } = require('@prezzemolo/zip') + +const migrate = async (user) => { + const result = await User.update(user._id, { + $unset: { + account: '' + }, + $set: { + host: null, + hostLower: null, + email: user.account.email, + links: user.account.links, + password: user.account.password, + token: user.account.token, + twitter: user.account.twitter, + line: user.account.line, + profile: user.account.profile, + lastUsedAt: user.account.lastUsedAt, + isBot: user.account.isBot, + isPro: user.account.isPro, + twoFactorSecret: user.account.twoFactorSecret, + twoFactorEnabled: user.account.twoFactorEnabled, + clientSettings: user.account.clientSettings, + settings: user.account.settings, + keypair: user.account.keypair + } + }); + return result.ok === 1; +} + +async function main() { + const count = await User.count({}); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await User.find({}, { + limit: dop, skip: time * dop + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) -- cgit v1.2.3-freya From 5ddb047794bb3aaef250c15dfbae56872faedfd9 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 05:02:50 +0900 Subject: Fix bugs --- src/remote/activitypub/renderer/follow.ts | 6 +- src/remote/activitypub/resolve-person.ts | 12 +-- src/remote/request.ts | 2 +- src/server/api/endpoints/following/create.ts | 2 +- src/server/api/endpoints/i/update.ts | 24 +---- src/server/api/endpoints/users/notes.ts | 137 +++++++++++++++++++++++++++ src/server/api/endpoints/users/posts.ts | 137 --------------------------- 7 files changed, 151 insertions(+), 169 deletions(-) create mode 100644 src/server/api/endpoints/users/notes.ts delete mode 100644 src/server/api/endpoints/users/posts.ts (limited to 'src/remote') diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts index 0a1ae1a4b2..89993d9458 100644 --- a/src/remote/activitypub/renderer/follow.ts +++ b/src/remote/activitypub/renderer/follow.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { IRemoteUser } from '../../../models/user'; +import { IRemoteUser, ILocalUser } from '../../../models/user'; -export default ({ username }, followee: IRemoteUser) => ({ +export default (follower: ILocalUser, followee: IRemoteUser) => ({ type: 'Follow', - actor: `${config.url}/@${username}`, + actor: `${config.url}/@${follower.username}`, object: followee.uri }); diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts index ac0900307f..ddb8d68715 100644 --- a/src/remote/activitypub/resolve-person.ts +++ b/src/remote/activitypub/resolve-person.ts @@ -66,14 +66,12 @@ export default async (value, verifier?: string) => { usernameLower: object.preferredUsername.toLowerCase(), host, hostLower, - account: { - publicKey: { - id: object.publicKey.id, - publicKeyPem: object.publicKey.publicKeyPem - }, - inbox: object.inbox, - uri: id, + publicKey: { + id: object.publicKey.id, + publicKeyPem: object.publicKey.publicKeyPem }, + inbox: object.inbox, + uri: id }); const [avatarId, bannerId] = (await Promise.all([ diff --git a/src/remote/request.ts b/src/remote/request.ts index a0c69cf4ef..81e7c05c73 100644 --- a/src/remote/request.ts +++ b/src/remote/request.ts @@ -8,7 +8,7 @@ import { ILocalUser } from '../models/user'; const log = debug('misskey:activitypub:deliver'); -export default (user: ILocalUser, url, object) => new Promise((resolve, reject) => { +export default (user: ILocalUser, url: string, object) => new Promise((resolve, reject) => { log(`--> ${url}`); const { protocol, hostname, port, pathname, search } = new URL(url); diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index d3cc549ca7..0a642f50b2 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }, { fields: { data: false, - 'profile': false + profile: false } }); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index a8caa0ebc4..36be2774f6 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -4,7 +4,6 @@ import $ from 'cafy'; import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user'; import event from '../../../../publishers/stream'; -import config from '../../../../config'; /** * Update myself @@ -17,7 +16,7 @@ import config from '../../../../config'; */ module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => { // Get 'name' parameter - const [name, nameErr] = $(params.name).optional.string().pipe(isValidName).$; + const [name, nameErr] = $(params.name).optional.nullable.string().pipe(isValidName).$; if (nameErr) return rej('invalid name param'); if (name) user.name = name; @@ -62,9 +61,9 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re description: user.description, avatarId: user.avatarId, bannerId: user.bannerId, - 'profile': user.profile, - 'isBot': user.isBot, - 'settings': user.settings + profile: user.profile, + isBot: user.isBot, + settings: user.settings } }); @@ -79,19 +78,4 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re // Publish i updated event event(user._id, 'i_updated', iObj); - - // Update search index - if (config.elasticsearch.enable) { - const es = require('../../../db/elasticsearch'); - - es.index({ - index: 'misskey', - type: 'user', - id: user._id.toString(), - body: { - name: user.name, - bio: user.bio - } - }); - } }); diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts new file mode 100644 index 0000000000..f9f6345e34 --- /dev/null +++ b/src/server/api/endpoints/users/notes.ts @@ -0,0 +1,137 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import getHostLower from '../../common/get-host-lower'; +import Note, { pack } from '../../../../models/note'; +import User from '../../../../models/user'; + +/** + * Get notes of a user + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).optional.id().$; + if (userIdErr) return rej('invalid userId param'); + + // Get 'username' parameter + const [username, usernameErr] = $(params.username).optional.string().$; + if (usernameErr) return rej('invalid username param'); + + if (userId === undefined && username === undefined) { + return rej('userId or pair of username and host is required'); + } + + // Get 'host' parameter + const [host, hostErr] = $(params.host).optional.string().$; + if (hostErr) return rej('invalid host param'); + + if (userId === undefined && host === undefined) { + return rej('userId or pair of username and host is required'); + } + + // Get 'includeReplies' parameter + const [includeReplies = true, includeRepliesErr] = $(params.includeReplies).optional.boolean().$; + if (includeRepliesErr) return rej('invalid includeReplies param'); + + // Get 'withMedia' parameter + const [withMedia = false, withMediaErr] = $(params.withMedia).optional.boolean().$; + if (withMediaErr) return rej('invalid withMedia param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; + + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + if (untilDateErr) throw 'invalid untilDate param'; + + // Check if only one of sinceId, untilId, sinceDate, untilDate specified + if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; + } + + const q = userId !== undefined + ? { _id: userId } + : { usernameLower: username.toLowerCase(), hostLower: getHostLower(host) } ; + + // Lookup user + const user = await User.findOne(q, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + //#region Construct query + const sort = { + _id: -1 + }; + + const query = { + userId: user._id + } as any; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } else if (sinceDate) { + sort._id = 1; + query.createdAt = { + $gt: new Date(sinceDate) + }; + } else if (untilDate) { + query.createdAt = { + $lt: new Date(untilDate) + }; + } + + if (!includeReplies) { + query.replyId = null; + } + + if (withMedia) { + query.mediaIds = { + $exists: true, + $ne: null + }; + } + //#endregion + + // Issue query + const notes = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(notes.map(async (note) => + await pack(note, me) + ))); +}); diff --git a/src/server/api/endpoints/users/posts.ts b/src/server/api/endpoints/users/posts.ts deleted file mode 100644 index f9f6345e34..0000000000 --- a/src/server/api/endpoints/users/posts.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import getHostLower from '../../common/get-host-lower'; -import Note, { pack } from '../../../../models/note'; -import User from '../../../../models/user'; - -/** - * Get notes of a user - * - * @param {any} params - * @param {any} me - * @return {Promise} - */ -module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).optional.id().$; - if (userIdErr) return rej('invalid userId param'); - - // Get 'username' parameter - const [username, usernameErr] = $(params.username).optional.string().$; - if (usernameErr) return rej('invalid username param'); - - if (userId === undefined && username === undefined) { - return rej('userId or pair of username and host is required'); - } - - // Get 'host' parameter - const [host, hostErr] = $(params.host).optional.string().$; - if (hostErr) return rej('invalid host param'); - - if (userId === undefined && host === undefined) { - return rej('userId or pair of username and host is required'); - } - - // Get 'includeReplies' parameter - const [includeReplies = true, includeRepliesErr] = $(params.includeReplies).optional.boolean().$; - if (includeRepliesErr) return rej('invalid includeReplies param'); - - // Get 'withMedia' parameter - const [withMedia = false, withMediaErr] = $(params.withMedia).optional.boolean().$; - if (withMediaErr) return rej('invalid withMedia param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; - if (sinceIdErr) return rej('invalid sinceId param'); - - // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; - if (untilIdErr) return rej('invalid untilId param'); - - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; - if (sinceDateErr) throw 'invalid sinceDate param'; - - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; - if (untilDateErr) throw 'invalid untilDate param'; - - // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { - throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; - } - - const q = userId !== undefined - ? { _id: userId } - : { usernameLower: username.toLowerCase(), hostLower: getHostLower(host) } ; - - // Lookup user - const user = await User.findOne(q, { - fields: { - _id: true - } - }); - - if (user === null) { - return rej('user not found'); - } - - //#region Construct query - const sort = { - _id: -1 - }; - - const query = { - userId: user._id - } as any; - - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (untilId) { - query._id = { - $lt: untilId - }; - } else if (sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(sinceDate) - }; - } else if (untilDate) { - query.createdAt = { - $lt: new Date(untilDate) - }; - } - - if (!includeReplies) { - query.replyId = null; - } - - if (withMedia) { - query.mediaIds = { - $exists: true, - $ne: null - }; - } - //#endregion - - // Issue query - const notes = await Note - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(notes.map(async (note) => - await pack(note, me) - ))); -}); -- cgit v1.2.3-freya From 0004944708c049aba54bb61898d5dc2bf3c97f14 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 05:07:17 +0900 Subject: Fix bug --- src/remote/activitypub/act/follow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/remote') diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts index 3dd029af54..236886dc60 100644 --- a/src/remote/activitypub/act/follow.ts +++ b/src/remote/activitypub/act/follow.ts @@ -6,7 +6,7 @@ import { IFollow } from '../type'; export default async (actor: IRemoteUser, activity: IFollow): Promise => { const prefix = config.url + '/@'; - const id = typeof activity == 'string' ? activity : activity.id; + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; if (!id.startsWith(prefix)) { return null; -- cgit v1.2.3-freya From 6e34e77372bd74c85ebf5a6b4214c818231dbe8b Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 06:55:26 +0900 Subject: Implement announce And bug fixes --- src/models/note.ts | 2 +- src/models/notification.ts | 3 - src/remote/activitypub/act/announce/index.ts | 39 +++++ src/remote/activitypub/act/announce/note.ts | 52 ++++++ src/remote/activitypub/act/index.ts | 5 + src/remote/activitypub/act/like.ts | 2 +- src/remote/activitypub/renderer/announce.ts | 4 + src/remote/activitypub/renderer/like.ts | 3 +- src/remote/activitypub/renderer/note.ts | 8 +- src/remote/activitypub/resolve-person.ts | 10 +- src/remote/activitypub/resolver.ts | 7 + src/remote/activitypub/type.ts | 15 +- src/server/activitypub/note.ts | 23 +-- src/server/activitypub/outbox.ts | 2 +- src/server/api/endpoints/posts/create.ts | 251 --------------------------- src/services/note/create.ts | 32 ++-- src/services/note/reaction/create.ts | 6 +- 17 files changed, 164 insertions(+), 300 deletions(-) create mode 100644 src/remote/activitypub/act/announce/index.ts create mode 100644 src/remote/activitypub/act/announce/note.ts create mode 100644 src/remote/activitypub/renderer/announce.ts delete mode 100644 src/server/api/endpoints/posts/create.ts (limited to 'src/remote') diff --git a/src/models/note.ts b/src/models/note.ts index 9b33bb76f1..f509fa66c8 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -112,7 +112,7 @@ export const pack = async ( _note = deepcopy(note); } - if (!_note) throw 'invalid note arg.'; + if (!_note) throw `invalid note arg ${note}`; const id = _note._id; diff --git a/src/models/notification.ts b/src/models/notification.ts index 17144d7ee8..d5ca7135b7 100644 --- a/src/models/notification.ts +++ b/src/models/notification.ts @@ -51,9 +51,6 @@ export interface INotification { /** * Pack a notification for API response - * - * @param {any} notification - * @return {Promise} */ export const pack = (notification: any) => new Promise(async (resolve, reject) => { let _notification: any; diff --git a/src/remote/activitypub/act/announce/index.ts b/src/remote/activitypub/act/announce/index.ts new file mode 100644 index 0000000000..c3ac06607d --- /dev/null +++ b/src/remote/activitypub/act/announce/index.ts @@ -0,0 +1,39 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import announceNote from './note'; +import { IAnnounce } from '../../type'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IAnnounce): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Announce: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Note': + announceNote(resolver, actor, activity, object); + break; + + default: + console.warn(`Unknown announce type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/act/announce/note.ts b/src/remote/activitypub/act/announce/note.ts new file mode 100644 index 0000000000..24d159f184 --- /dev/null +++ b/src/remote/activitypub/act/announce/note.ts @@ -0,0 +1,52 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import Note from '../../../../models/note'; +import post from '../../../../services/note/create'; +import { IRemoteUser, isRemoteUser } from '../../../../models/user'; +import { IAnnounce, INote } from '../../type'; +import createNote from '../create/note'; +import resolvePerson from '../../resolve-person'; + +const log = debug('misskey:activitypub'); + +/** + * アナウンスアクティビティを捌きます + */ +export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise { + const uri = activity.id || activity; + + if (typeof uri !== 'string') { + throw new Error('invalid announce'); + } + + // 既に同じURIを持つものが登録されていないかチェック + const exist = await Note.findOne({ uri }); + if (exist) { + return; + } + + // アナウンス元の投稿の投稿者をフェッチ + const announcee = await resolvePerson(note.attributedTo); + + const renote = isRemoteUser(announcee) + ? await createNote(resolver, announcee, note, true) + : await Note.findOne({ _id: note.id.split('/').pop() }); + + log(`Creating the (Re)Note: ${uri}`); + + //#region Visibility + let visibility = 'public'; + if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; + if (activity.cc.length == 0) visibility = 'private'; + // TODO + if (visibility != 'public') throw new Error('unspported visibility'); + //#endergion + + await post(actor, { + createdAt: new Date(activity.published), + renote, + visibility, + uri + }); +} diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts index 45d7bd16a9..15ea9494ae 100644 --- a/src/remote/activitypub/act/index.ts +++ b/src/remote/activitypub/act/index.ts @@ -5,6 +5,7 @@ import performDeleteActivity from './delete'; import follow from './follow'; import undo from './undo'; import like from './like'; +import announce from './announce'; const self = async (actor: IRemoteUser, activity: Object): Promise => { switch (activity.type) { @@ -24,6 +25,10 @@ const self = async (actor: IRemoteUser, activity: Object): Promise => { // noop break; + case 'Announce': + await announce(actor, activity); + break; + case 'Like': await like(actor, activity); break; diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/act/like.ts index a3243948ba..4941608588 100644 --- a/src/remote/activitypub/act/like.ts +++ b/src/remote/activitypub/act/like.ts @@ -7,7 +7,7 @@ export default async (actor: IRemoteUser, activity: ILike) => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; // Transform: - // https://misskey.ex/@syuilo/xxxx to + // https://misskey.ex/notes/xxxx to // xxxx const noteId = id.split('/').pop(); 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/like.ts b/src/remote/activitypub/renderer/like.ts index fe36c7094d..744896cc41 100644 --- a/src/remote/activitypub/renderer/like.ts +++ b/src/remote/activitypub/renderer/like.ts @@ -1,6 +1,7 @@ import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; -export default (user, note) => { +export default (user: ILocalUser, note) => { return { type: 'Like', actor: `${config.url}/@${user.username}`, diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index 244aecf6a3..48799af084 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -3,9 +3,9 @@ import renderHashtag from './hashtag'; import config from '../../../config'; import DriveFile from '../../../models/drive-file'; import Note, { INote } from '../../../models/note'; -import User, { IUser } from '../../../models/user'; +import User from '../../../models/user'; -export default async (user: IUser, note: INote) => { +export default async (note: INote) => { const promisedFiles = note.mediaIds ? DriveFile.find({ _id: { $in: note.mediaIds } }) : Promise.resolve([]); @@ -30,6 +30,10 @@ export default async (user: IUser, note: INote) => { inReplyTo = null; } + const user = await User.findOne({ + _id: note.userId + }); + const attributedTo = `${config.url}/@${user.username}`; return { diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts index ddb8d68715..7d7989a01f 100644 --- a/src/remote/activitypub/resolve-person.ts +++ b/src/remote/activitypub/resolve-person.ts @@ -2,18 +2,18 @@ import { JSDOM } from 'jsdom'; import { toUnicode } from 'punycode'; import parseAcct from '../../acct/parse'; import config from '../../config'; -import User, { validateUsername, isValidName, isValidDescription } from '../../models/user'; +import User, { validateUsername, isValidName, isValidDescription, IUser } from '../../models/user'; import webFinger from '../webfinger'; import Resolver from './resolver'; import uploadFromUrl from '../../services/drive/upload-from-url'; -import { isCollectionOrOrderedCollection } from './type'; +import { isCollectionOrOrderedCollection, IObject } from './type'; -export default async (value, verifier?: string) => { - const id = value.id || value; +export default async (value: string | IObject, verifier?: string): Promise => { + const id = typeof value == 'string' ? value : value.id; const localPrefix = config.url + '/@'; if (id.startsWith(localPrefix)) { - return User.findOne(parseAcct(id.slice(localPrefix))); + return await User.findOne(parseAcct(id.substr(localPrefix.length))); } const resolver = new Resolver(); diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 4a97e2ef66..1466139c48 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -1,6 +1,7 @@ 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'); @@ -47,6 +48,11 @@ export default class Resolver { this.history.add(value); + //#region resolve local objects + // TODO + //if (value.startsWith(`${config.url}/@`)) { + //#endregion + const object = await request({ url: value, headers: { @@ -60,6 +66,7 @@ export default class Resolver { !object['@context'].includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' )) { + log(`invalid response: ${JSON.stringify(object, null, 2)}`); throw new Error('invalid response'); } diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 450d5906d8..2335517645 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -5,6 +5,10 @@ export interface IObject { type: string; id?: string; summary?: string; + published?: string; + cc?: string[]; + to?: string[]; + attributedTo: string; } export interface IActivity extends IObject { @@ -26,6 +30,10 @@ export interface IOrderedCollection extends IObject { orderedItems: IObject | string | IObject[] | string[]; } +export interface INote extends IObject { + type: 'Note'; +} + export const isCollection = (object: IObject): object is ICollection => object.type === 'Collection'; @@ -59,6 +67,10 @@ export interface ILike extends IActivity { type: 'Like'; } +export interface IAnnounce extends IActivity { + type: 'Announce'; +} + export type Object = ICollection | IOrderedCollection | @@ -67,4 +79,5 @@ export type Object = IUndo | IFollow | IAccept | - ILike; + ILike | + IAnnounce; diff --git a/src/server/activitypub/note.ts b/src/server/activitypub/note.ts index cea9be52da..1c2e695b80 100644 --- a/src/server/activitypub/note.ts +++ b/src/server/activitypub/note.ts @@ -1,40 +1,25 @@ import * as express from 'express'; import context from '../../remote/activitypub/renderer/context'; import render from '../../remote/activitypub/renderer/note'; -import parseAcct from '../../acct/parse'; import Note from '../../models/note'; -import User from '../../models/user'; const app = express.Router(); -app.get('/@:user/:note', async (req, res, next) => { +app.get('/notes/:note', async (req, res, next) => { const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']); if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) { return next(); } - const { username, host } = parseAcct(req.params.user); - if (host !== null) { - return res.sendStatus(422); - } - - const user = await User.findOne({ - usernameLower: username.toLowerCase(), - host: null - }); - if (user === null) { - return res.sendStatus(404); - } - const note = await Note.findOne({ - _id: req.params.note, - userId: user._id + _id: req.params.note }); + if (note === null) { return res.sendStatus(404); } - const rendered = await render(user, note); + const rendered = await render(note); rendered['@context'] = context; res.json(rendered); diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts index b6f3a3f9db..4557871bc5 100644 --- a/src/server/activitypub/outbox.ts +++ b/src/server/activitypub/outbox.ts @@ -16,7 +16,7 @@ app.get('/@:user/outbox', withUser(username => { sort: { _id: -1 } }); - const renderedNotes = await Promise.all(notes.map(note => renderNote(user, note))); + const renderedNotes = await Promise.all(notes.map(note => renderNote(note))); const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.notesCount, renderedNotes); rendered['@context'] = context; diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts deleted file mode 100644 index 7e79912b1b..0000000000 --- a/src/server/api/endpoints/posts/create.ts +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import deepEqual = require('deep-equal'); -import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note'; -import { ILocalUser } from '../../../../models/user'; -import Channel, { IChannel } from '../../../../models/channel'; -import DriveFile from '../../../../models/drive-file'; -import create from '../../../../services/note/create'; -import { IApp } from '../../../../models/app'; - -/** - * Create a note - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise} - */ -module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => { - // Get 'visibility' parameter - const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).$; - if (visibilityErr) return rej('invalid visibility'); - - // Get 'text' parameter - const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; - if (textErr) return rej('invalid text'); - - // Get 'cw' parameter - const [cw, cwErr] = $(params.cw).optional.string().pipe(isValidCw).$; - if (cwErr) return rej('invalid cw'); - - // Get 'viaMobile' parameter - const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$; - if (viaMobileErr) return rej('invalid viaMobile'); - - // Get 'tags' parameter - const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$; - if (tagsErr) return rej('invalid tags'); - - // Get 'geo' parameter - const [geo, geoErr] = $(params.geo).optional.nullable.strict.object() - .have('coordinates', $().array().length(2) - .item(0, $().number().range(-180, 180)) - .item(1, $().number().range(-90, 90))) - .have('altitude', $().nullable.number()) - .have('accuracy', $().nullable.number()) - .have('altitudeAccuracy', $().nullable.number()) - .have('heading', $().nullable.number().range(0, 360)) - .have('speed', $().nullable.number()) - .$; - if (geoErr) return rej('invalid geo'); - - // Get 'mediaIds' parameter - const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$; - if (mediaIdsErr) return rej('invalid mediaIds'); - - let files = []; - if (mediaIds !== undefined) { - // Fetch files - // forEach だと途中でエラーなどがあっても return できないので - // 敢えて for を使っています。 - for (const mediaId of mediaIds) { - // Fetch file - // SELECT _id - const entity = await DriveFile.findOne({ - _id: mediaId, - 'metadata.userId': user._id - }); - - if (entity === null) { - return rej('file not found'); - } else { - files.push(entity); - } - } - } else { - files = null; - } - - // Get 'renoteId' parameter - const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$; - if (renoteIdErr) return rej('invalid renoteId'); - - let renote: INote = null; - let isQuote = false; - if (renoteId !== undefined) { - // Fetch renote to note - renote = await Note.findOne({ - _id: renoteId - }); - - if (renote == null) { - return rej('renoteee is not found'); - } else if (renote.renoteId && !renote.text && !renote.mediaIds) { - return rej('cannot renote to renote'); - } - - // Fetch recently note - const latestNote = await Note.findOne({ - userId: user._id - }, { - sort: { - _id: -1 - } - }); - - isQuote = text != null || files != null; - - // 直近と同じRenote対象かつ引用じゃなかったらエラー - if (latestNote && - latestNote.renoteId && - latestNote.renoteId.equals(renote._id) && - !isQuote) { - return rej('cannot renote same note that already reposted in your latest note'); - } - - // 直近がRenote対象かつ引用じゃなかったらエラー - if (latestNote && - latestNote._id.equals(renote._id) && - !isQuote) { - return rej('cannot renote your latest note'); - } - } - - // Get 'replyId' parameter - const [replyId, replyIdErr] = $(params.replyId).optional.id().$; - if (replyIdErr) return rej('invalid replyId'); - - let reply: INote = null; - if (replyId !== undefined) { - // Fetch reply - reply = await Note.findOne({ - _id: replyId - }); - - if (reply === null) { - return rej('in reply to note is not found'); - } - - // 返信対象が引用でないRenoteだったらエラー - if (reply.renoteId && !reply.text && !reply.mediaIds) { - return rej('cannot reply to renote'); - } - } - - // Get 'channelId' parameter - const [channelId, channelIdErr] = $(params.channelId).optional.id().$; - if (channelIdErr) return rej('invalid channelId'); - - let channel: IChannel = null; - if (channelId !== undefined) { - // Fetch channel - channel = await Channel.findOne({ - _id: channelId - }); - - if (channel === null) { - return rej('channel not found'); - } - - // 返信対象の投稿がこのチャンネルじゃなかったらダメ - if (reply && !channelId.equals(reply.channelId)) { - return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません'); - } - - // Renote対象の投稿がこのチャンネルじゃなかったらダメ - if (renote && !channelId.equals(renote.channelId)) { - return rej('チャンネル内部からチャンネル外部の投稿をRenoteすることはできません'); - } - - // 引用ではないRenoteはダメ - if (renote && !isQuote) { - return rej('チャンネル内部では引用ではないRenoteをすることはできません'); - } - } else { - // 返信対象の投稿がチャンネルへの投稿だったらダメ - if (reply && reply.channelId != null) { - return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません'); - } - - // Renote対象の投稿がチャンネルへの投稿だったらダメ - if (renote && renote.channelId != null) { - return rej('チャンネル外部からチャンネル内部の投稿をRenoteすることはできません'); - } - } - - // Get 'poll' parameter - const [poll, pollErr] = $(params.poll).optional.strict.object() - .have('choices', $().array('string') - .unique() - .range(2, 10) - .each(c => c.length > 0 && c.length < 50)) - .$; - if (pollErr) return rej('invalid poll'); - - if (poll) { - (poll as any).choices = (poll as any).choices.map((choice, i) => ({ - id: i, // IDを付与 - text: choice.trim(), - votes: 0 - })); - } - - // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー - if (text === undefined && files === null && renote === null && poll === undefined) { - return rej('text, mediaIds, renoteId or poll is required'); - } - - // 直近の投稿と重複してたらエラー - // TODO: 直近の投稿が一日前くらいなら重複とは見なさない - if (user.latestNote) { - if (deepEqual({ - text: user.latestNote.text, - reply: user.latestNote.replyId ? user.latestNote.replyId.toString() : null, - renote: user.latestNote.renoteId ? user.latestNote.renoteId.toString() : null, - mediaIds: (user.latestNote.mediaIds || []).map(id => id.toString()) - }, { - text: text, - reply: reply ? reply._id.toString() : null, - renote: renote ? renote._id.toString() : null, - mediaIds: (files || []).map(file => file._id.toString()) - })) { - return rej('duplicate'); - } - } - - // 投稿を作成 - const note = await create(user, { - createdAt: new Date(), - media: files, - poll: poll, - text: text, - reply, - renote, - cw: cw, - tags: tags, - app: app, - viaMobile: viaMobile, - visibility, - geo - }); - - const noteObj = await pack(note, user); - - // Reponse - res({ - createdNote: noteObj - }); -}); diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 551d618569..aac207cc12 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -5,6 +5,7 @@ import Following from '../../models/following'; import { deliver } from '../../queue'; import renderNote from '../../remote/activitypub/renderer/note'; import renderCreate from '../../remote/activitypub/renderer/create'; +import renderAnnounce from '../../remote/activitypub/renderer/announce'; import context from '../../remote/activitypub/renderer/context'; import { IDriveFile } from '../../models/drive-file'; import notify from '../../publishers/notify'; @@ -34,6 +35,7 @@ export default async (user: IUser, data: { }, silent = false) => new Promise(async (res, rej) => { if (data.createdAt == null) data.createdAt = new Date(); if (data.visibility == null) data.visibility = 'public'; + if (data.viaMobile == null) data.viaMobile = false; const tags = data.tags || []; @@ -77,9 +79,7 @@ export default async (user: IUser, data: { _user: { host: user.host, hostLower: user.hostLower, - account: isLocalUser(user) ? {} : { - inbox: user.inbox - } + inbox: isRemoteUser(user) ? user.inbox : undefined } }; @@ -128,15 +128,25 @@ export default async (user: IUser, data: { }); if (!silent) { - const content = renderCreate(await renderNote(user, note)); - content['@context'] = context; + const render = async () => { + const content = data.renote && data.text == null + ? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote)) + : renderCreate(await renderNote(note)); + content['@context'] = context; + return content; + }; // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) { - deliver(user, content, data.reply._user.inbox).save(); + deliver(user, await render(), data.reply._user.inbox).save(); + } + + // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 + if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) { + deliver(user, await render(), data.renote._user.inbox).save(); } - Promise.all(followers.map(follower => { + Promise.all(followers.map(async follower => { follower = follower.user[0]; if (isLocalUser(follower)) { @@ -145,7 +155,7 @@ export default async (user: IUser, data: { } else { // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 if (isLocalUser(user)) { - deliver(user, content, follower.inbox).save(); + deliver(user, await render(), follower.inbox).save(); } } })); @@ -255,15 +265,13 @@ export default async (user: IUser, data: { // Notify const type = data.text ? 'quote' : 'renote'; notify(data.renote.userId, user._id, type, { - note_id: note._id + noteId: note._id }); // Fetch watchers NoteWatching.find({ noteId: data.renote._id, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } + userId: { $ne: user._id } }, { fields: { userId: true diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index ea51b205d0..88158034f3 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -83,11 +83,11 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise } //#region 配信 - const content = renderLike(user, note); - content['@context'] = context; - // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 if (isLocalUser(user) && isRemoteUser(note._user)) { + const content = renderLike(user, note); + content['@context'] = context; + deliver(user, content, note._user.inbox).save(); } //#endregion -- cgit v1.2.3-freya From 04cdb04ee2d80ce37a7fd6d8e7dfa47cff539d8f Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 07:11:29 +0900 Subject: Fix bug --- src/remote/activitypub/act/create/image.ts | 2 +- src/remote/activitypub/act/create/note.ts | 10 ++++------ src/server/api/endpoints/drive/files/upload_from_url.ts | 4 ---- 3 files changed, 5 insertions(+), 11 deletions(-) (limited to 'src/remote') diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts index c87423c5fd..f1462f4ee4 100644 --- a/src/remote/activitypub/act/create/image.ts +++ b/src/remote/activitypub/act/create/image.ts @@ -12,7 +12,7 @@ export default async function(actor: IRemoteUser, image): Promise { throw new Error('invalid image'); } - log(`Creating the Image: ${image.id}`); + log(`Creating the Image: ${image.url}`); return await uploadFromUrl(image.url, actor); } diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts index 572a293ab4..599bc10aa8 100644 --- a/src/remote/activitypub/act/create/note.ts +++ b/src/remote/activitypub/act/create/note.ts @@ -37,15 +37,13 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser, //#endergion //#region 添付メディア - const media = []; + let 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); - }); + media = await Promise.all(note.attachment.map(x => { + return createImage(actor, x); + })); } //#endregion diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts index 9ebc8b8233..8a426c0efc 100644 --- a/src/server/api/endpoints/drive/files/upload_from_url.ts +++ b/src/server/api/endpoints/drive/files/upload_from_url.ts @@ -7,10 +7,6 @@ import uploadFromUrl from '../../../../../services/drive/upload-from-url'; /** * Create a file from a URL - * - * @param {any} params - * @param {any} user - * @return {Promise} */ module.exports = async (params, user): Promise => { // Get 'url' parameter -- cgit v1.2.3-freya From e63f884bc6ba89902e2efd20f1c6d8939f7c4270 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 15:15:22 +0900 Subject: Use id in uri instead of username --- src/remote/activitypub/act/follow.ts | 16 +++++++--------- src/remote/activitypub/act/undo/follow.ts | 18 ++++++++---------- src/remote/activitypub/renderer/follow.ts | 2 +- src/remote/activitypub/renderer/key.ts | 4 ++-- src/remote/activitypub/renderer/like.ts | 12 +++++------- src/remote/activitypub/renderer/note.ts | 2 +- src/remote/activitypub/renderer/person.ts | 2 +- src/remote/activitypub/resolve-person.ts | 6 ++---- src/remote/activitypub/resolver.ts | 2 +- src/server/activitypub/inbox.ts | 2 +- src/server/activitypub/outbox.ts | 14 ++++++++------ src/server/activitypub/publickey.ts | 13 +++++++------ src/server/activitypub/user.ts | 23 ++++++++--------------- src/server/activitypub/with-user.ts | 23 ----------------------- 14 files changed, 52 insertions(+), 87 deletions(-) delete mode 100644 src/server/activitypub/with-user.ts (limited to 'src/remote') diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts index 236886dc60..6a8b5a1bec 100644 --- a/src/remote/activitypub/act/follow.ts +++ b/src/remote/activitypub/act/follow.ts @@ -1,25 +1,23 @@ -import parseAcct from '../../../acct/parse'; import User, { IRemoteUser } from '../../../models/user'; import config from '../../../config'; import follow from '../../../services/following/create'; import { IFollow } from '../type'; export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const prefix = config.url + '/@'; const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - if (!id.startsWith(prefix)) { + if (!id.startsWith(config.url + '/')) { return null; } - const { username, host } = parseAcct(id.slice(prefix.length)); - if (host !== null) { - throw new Error(); - } + const followee = await User.findOne({ _id: id.split('/').pop() }); - const followee = await User.findOne({ username, host }); if (followee === null) { - throw new Error(); + throw new Error('followee not found'); + } + + if (followee.host != null) { + throw new Error('フォローしようとしているユーザーはローカルユーザーではありません'); } await follow(actor, followee, activity); diff --git a/src/remote/activitypub/act/undo/follow.ts b/src/remote/activitypub/act/undo/follow.ts index fcf27c9507..a85cb0305d 100644 --- a/src/remote/activitypub/act/undo/follow.ts +++ b/src/remote/activitypub/act/undo/follow.ts @@ -1,25 +1,23 @@ -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 => { - const prefix = config.url + '/@'; - const id = typeof activity == 'string' ? activity : activity.id; + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - if (!id.startsWith(prefix)) { + if (!id.startsWith(config.url + '/')) { return null; } - const { username, host } = parseAcct(id.slice(prefix.length)); - if (host !== null) { - throw new Error(); - } + const followee = await User.findOne({ _id: id.split('/').pop() }); - const followee = await User.findOne({ username, host }); if (followee === null) { - throw new Error(); + throw new Error('followee not found'); + } + + if (followee.host != null) { + throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); } await unfollow(actor, followee, activity); diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts index 89993d9458..bf8eeff06b 100644 --- a/src/remote/activitypub/renderer/follow.ts +++ b/src/remote/activitypub/renderer/follow.ts @@ -3,6 +3,6 @@ import { IRemoteUser, ILocalUser } from '../../../models/user'; export default (follower: ILocalUser, followee: IRemoteUser) => ({ type: 'Follow', - actor: `${config.url}/@${follower.username}`, + actor: `${config.url}/users/${follower._id}`, object: followee.uri }); diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts index 76e2f13bcc..0d5e52557c 100644 --- a/src/remote/activitypub/renderer/key.ts +++ b/src/remote/activitypub/renderer/key.ts @@ -3,8 +3,8 @@ import { extractPublic } from '../../../crypto_key'; import { ILocalUser } from '../../../models/user'; export default (user: ILocalUser) => ({ - id: `${config.url}/@${user.username}/publickey`, + id: `${config.url}/users/${user._id}/publickey`, type: 'Key', - owner: `${config.url}/@${user.username}`, + 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 index 744896cc41..061a10ba84 100644 --- a/src/remote/activitypub/renderer/like.ts +++ b/src/remote/activitypub/renderer/like.ts @@ -1,10 +1,8 @@ import config from '../../../config'; import { ILocalUser } from '../../../models/user'; -export default (user: ILocalUser, note) => { - return { - type: 'Like', - actor: `${config.url}/@${user.username}`, - object: note.uri ? note.uri : `${config.url}/notes/${note._id}` - }; -}; +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 index 48799af084..7cc388dc33 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -34,7 +34,7 @@ export default async (note: INote) => { _id: note.userId }); - const attributedTo = `${config.url}/@${user.username}`; + const attributedTo = `${config.url}/users/${user._id}`; return { id: `${config.url}/notes/${note._id}`, diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index 7ea6f532fb..82e261029c 100644 --- a/src/remote/activitypub/renderer/person.ts +++ b/src/remote/activitypub/renderer/person.ts @@ -3,7 +3,7 @@ import renderKey from './key'; import config from '../../../config'; export default user => { - const id = `${config.url}/@${user.username}`; + const id = `${config.url}/users/${user._id}`; return { type: 'Person', diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts index 7d7989a01f..0140811f0a 100644 --- a/src/remote/activitypub/resolve-person.ts +++ b/src/remote/activitypub/resolve-person.ts @@ -1,6 +1,5 @@ import { JSDOM } from 'jsdom'; import { toUnicode } from 'punycode'; -import parseAcct from '../../acct/parse'; import config from '../../config'; import User, { validateUsername, isValidName, isValidDescription, IUser } from '../../models/user'; import webFinger from '../webfinger'; @@ -10,10 +9,9 @@ import { isCollectionOrOrderedCollection, IObject } from './type'; export default async (value: string | IObject, verifier?: string): Promise => { const id = typeof value == 'string' ? value : value.id; - const localPrefix = config.url + '/@'; - if (id.startsWith(localPrefix)) { - return await User.findOne(parseAcct(id.substr(localPrefix.length))); + if (id.startsWith(config.url + '/')) { + return await User.findOne({ _id: id.split('/').pop() }); } const resolver = new Resolver(); diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 1466139c48..7d45783b47 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -50,7 +50,7 @@ export default class Resolver { //#region resolve local objects // TODO - //if (value.startsWith(`${config.url}/@`)) { + //if (value.startsWith(`${config.url}/`)) { //#endregion const object = await request({ diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts index 1b6cc0c00a..643d2945bd 100644 --- a/src/server/activitypub/inbox.ts +++ b/src/server/activitypub/inbox.ts @@ -5,7 +5,7 @@ import { createHttp } from '../../queue'; const app = express.Router(); -app.post('/@:user/inbox', bodyParser.json({ +app.post('/users/:user/inbox', bodyParser.json({ type() { return true; } diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts index 4557871bc5..1c97c17a2e 100644 --- a/src/server/activitypub/outbox.ts +++ b/src/server/activitypub/outbox.ts @@ -4,23 +4,25 @@ import renderNote from '../../remote/activitypub/renderer/note'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import config from '../../config'; import Note from '../../models/note'; -import withUser from './with-user'; +import User from '../../models/user'; const app = express.Router(); -app.get('/@:user/outbox', withUser(username => { - return `${config.url}/@${username}/inbox`; -}, async (user, req, res) => { +app.get('/users/:user/outbox', async (req, res) => { + const userId = req.params.user; + + const user = await User.findOne({ _id: userId }); + const notes = await Note.find({ userId: user._id }, { limit: 20, sort: { _id: -1 } }); const renderedNotes = await Promise.all(notes.map(note => renderNote(note))); - const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.notesCount, renderedNotes); + const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes); rendered['@context'] = context; res.json(rendered); -})); +}); export default app; diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts index b48504927a..aa0c4271b8 100644 --- a/src/server/activitypub/publickey.ts +++ b/src/server/activitypub/publickey.ts @@ -1,18 +1,19 @@ import * as express from 'express'; import context from '../../remote/activitypub/renderer/context'; import render from '../../remote/activitypub/renderer/key'; -import config from '../../config'; -import withUser from './with-user'; +import User from '../../models/user'; const app = express.Router(); -app.get('/@:user/publickey', withUser(username => { - return `${config.url}/@${username}/publickey`; -}, (user, req, res) => { +app.get('/users/:user/publickey', async (req, res) => { + const userId = req.params.user; + + const user = await User.findOne({ _id: userId }); + const rendered = render(user); rendered['@context'] = context; res.json(rendered); -})); +}); export default app; diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts index f054974510..9e98e92b6a 100644 --- a/src/server/activitypub/user.ts +++ b/src/server/activitypub/user.ts @@ -1,26 +1,19 @@ import * as express from 'express'; -import config from '../../config'; import context from '../../remote/activitypub/renderer/context'; import render from '../../remote/activitypub/renderer/person'; -import withUser from './with-user'; +import User from '../../models/user'; -const respond = withUser(username => `${config.url}/@${username}`, (user, req, res) => { - const rendered = render(user); - rendered['@context'] = context; +const app = express.Router(); - res.json(rendered); -}); +app.get('/users/:user', async (req, res) => { + const userId = req.params.user; -const app = express.Router(); + const user = await User.findOne({ _id: userId }); -app.get('/@:user', (req, res, next) => { - const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']); + const rendered = render(user); + rendered['@context'] = context; - if ((['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) { - respond(req, res, next); - } else { - next(); - } + res.json(rendered); }); export default app; diff --git a/src/server/activitypub/with-user.ts b/src/server/activitypub/with-user.ts deleted file mode 100644 index bdbbefb429..0000000000 --- a/src/server/activitypub/with-user.ts +++ /dev/null @@ -1,23 +0,0 @@ -import parseAcct from '../../acct/parse'; -import User from '../../models/user'; - -export default (redirect, respond) => async (req, res, next) => { - const { username, host } = parseAcct(req.params.user); - if (host !== null) { - return res.sendStatus(422); - } - - const user = await User.findOne({ - usernameLower: username.toLowerCase(), - host: null - }); - if (user === null) { - return res.sendStatus(404); - } - - if (username !== user.username) { - return res.redirect(redirect(user.username)); - } - - return respond(user, req, res, next); -}; -- cgit v1.2.3-freya From 6d49edc0ab7af36f5d0d54397ddb1eacbb9c18b4 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 15:25:17 +0900 Subject: :v: --- src/remote/resolve-user.ts | 5 +++++ src/server/api/endpoints/users/show.ts | 4 ---- src/server/webfinger.ts | 9 ++++++--- 3 files changed, 11 insertions(+), 7 deletions(-) (limited to 'src/remote') diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts index 9e1ae51952..0e7edd8e12 100644 --- a/src/remote/resolve-user.ts +++ b/src/remote/resolve-user.ts @@ -2,12 +2,17 @@ import { toUnicode, toASCII } from 'punycode'; import User from '../models/user'; import resolvePerson from './activitypub/resolve-person'; import webFinger from './webfinger'; +import config from '../config'; export default async (username, host, option) => { const usernameLower = username.toLowerCase(); const hostLowerAscii = toASCII(host).toLowerCase(); const hostLower = toUnicode(hostLowerAscii); + if (config.host == hostLower) { + return await User.findOne({ usernameLower }); + } + let user = await User.findOne({ usernameLower, hostLower }, option); if (user === null) { diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts index d272ce4639..7e7f5dc488 100644 --- a/src/server/api/endpoints/users/show.ts +++ b/src/server/api/endpoints/users/show.ts @@ -9,10 +9,6 @@ const cursorOption = { fields: { data: false } }; /** * Show a user - * - * @param {any} params - * @param {any} me - * @return {Promise} */ module.exports = (params, me) => new Promise(async (res, rej) => { let user; diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts index fd7ebc3fb5..dbf0999f3e 100644 --- a/src/server/webfinger.ts +++ b/src/server/webfinger.ts @@ -4,9 +4,9 @@ import config from '../config'; import parseAcct from '../acct/parse'; import User from '../models/user'; -const app = express(); +const app = express.Router(); -app.get('/.well-known/webfinger', async (req: express.Request, res: express.Response) => { +app.get('/.well-known/webfinger', async (req, res) => { if (typeof req.query.resource !== 'string') { return res.sendStatus(400); } @@ -38,11 +38,14 @@ app.get('/.well-known/webfinger', async (req: express.Request, res: express.Resp links: [{ rel: 'self', type: 'application/activity+json', - href: `${config.url}/@${user.username}` + href: `${config.url}/users/${user._id}` }, { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: `${config.url}/@${user.username}` + }, { + rel: 'http://ostatus.org/schema/1.0/subscribe', + template: `${config.url}/authorize-follow?acct={uri}` }] }); }); -- cgit v1.2.3-freya From a1e6bb6fa3333ee3a01c2dced90f893b9fbefd3f Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 18:21:02 +0900 Subject: Provide url property --- src/remote/activitypub/renderer/person.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'src/remote') diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index 82e261029c..f1c8056a75 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`, + url: `${config.url}/@${user.username}`, preferredUsername: user.username, name: user.name, summary: user.description, -- cgit v1.2.3-freya From ad1a86113b7f562b6b25f7705d92a80473317aea Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 23:29:27 +0900 Subject: Fix bug --- src/models/user.ts | 1 + src/remote/activitypub/resolve-person.ts | 8 ++++++++ 2 files changed, 9 insertions(+) (limited to 'src/remote') diff --git a/src/models/user.ts b/src/models/user.ts index 906bcb533b..ea59730e4d 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -12,6 +12,7 @@ const User = db.get('users'); User.createIndex('username'); User.createIndex('token'); +User.createIndex('uri', { sparse: true, unique: true }); export default User; diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts index 0140811f0a..50e7873cbd 100644 --- a/src/remote/activitypub/resolve-person.ts +++ b/src/remote/activitypub/resolve-person.ts @@ -12,6 +12,14 @@ export default async (value: string | IObject, verifier?: string): Promise Date: Mon, 9 Apr 2018 04:08:56 +0900 Subject: wip --- src/queue/processors/http/process-inbox.ts | 6 +- src/remote/activitypub/act/announce/index.ts | 39 ------- src/remote/activitypub/act/announce/note.ts | 52 --------- src/remote/activitypub/act/create/image.ts | 18 --- src/remote/activitypub/act/create/index.ts | 44 ------- src/remote/activitypub/act/create/note.ts | 87 -------------- src/remote/activitypub/act/delete/index.ts | 36 ------ src/remote/activitypub/act/delete/note.ts | 30 ----- src/remote/activitypub/act/follow.ts | 24 ---- src/remote/activitypub/act/index.ts | 51 -------- src/remote/activitypub/act/like.ts | 20 ---- src/remote/activitypub/act/undo/follow.ts | 24 ---- src/remote/activitypub/act/undo/index.ts | 37 ------ src/remote/activitypub/objects/image.ts | 29 +++++ src/remote/activitypub/objects/note.ts | 110 ++++++++++++++++++ src/remote/activitypub/objects/person.ts | 142 +++++++++++++++++++++++ src/remote/activitypub/perform/announce/index.ts | 39 +++++++ src/remote/activitypub/perform/announce/note.ts | 45 +++++++ src/remote/activitypub/perform/create/image.ts | 6 + src/remote/activitypub/perform/create/index.ts | 44 +++++++ src/remote/activitypub/perform/create/note.ts | 13 +++ src/remote/activitypub/perform/delete/index.ts | 36 ++++++ src/remote/activitypub/perform/delete/note.ts | 30 +++++ src/remote/activitypub/perform/follow.ts | 24 ++++ src/remote/activitypub/perform/index.ts | 51 ++++++++ src/remote/activitypub/perform/like.ts | 20 ++++ src/remote/activitypub/perform/undo/follow.ts | 24 ++++ src/remote/activitypub/perform/undo/index.ts | 37 ++++++ src/remote/activitypub/resolve-person.ts | 98 ---------------- src/remote/activitypub/type.ts | 16 +++ src/remote/resolve-user.ts | 6 +- src/remote/webfinger.ts | 27 +---- 32 files changed, 678 insertions(+), 587 deletions(-) delete mode 100644 src/remote/activitypub/act/announce/index.ts delete mode 100644 src/remote/activitypub/act/announce/note.ts delete mode 100644 src/remote/activitypub/act/create/image.ts delete mode 100644 src/remote/activitypub/act/create/index.ts delete mode 100644 src/remote/activitypub/act/create/note.ts delete mode 100644 src/remote/activitypub/act/delete/index.ts delete mode 100644 src/remote/activitypub/act/delete/note.ts delete mode 100644 src/remote/activitypub/act/follow.ts delete mode 100644 src/remote/activitypub/act/index.ts delete mode 100644 src/remote/activitypub/act/like.ts delete mode 100644 src/remote/activitypub/act/undo/follow.ts delete mode 100644 src/remote/activitypub/act/undo/index.ts create mode 100644 src/remote/activitypub/objects/image.ts create mode 100644 src/remote/activitypub/objects/note.ts create mode 100644 src/remote/activitypub/objects/person.ts create mode 100644 src/remote/activitypub/perform/announce/index.ts create mode 100644 src/remote/activitypub/perform/announce/note.ts create mode 100644 src/remote/activitypub/perform/create/image.ts create mode 100644 src/remote/activitypub/perform/create/index.ts create mode 100644 src/remote/activitypub/perform/create/note.ts create mode 100644 src/remote/activitypub/perform/delete/index.ts create mode 100644 src/remote/activitypub/perform/delete/note.ts create mode 100644 src/remote/activitypub/perform/follow.ts create mode 100644 src/remote/activitypub/perform/index.ts create mode 100644 src/remote/activitypub/perform/like.ts create mode 100644 src/remote/activitypub/perform/undo/follow.ts create mode 100644 src/remote/activitypub/perform/undo/index.ts delete mode 100644 src/remote/activitypub/resolve-person.ts (limited to 'src/remote') diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index 6608907a7a..ce5b7d5a89 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -4,8 +4,8 @@ import * as debug from 'debug'; import { verifySignature } from 'http-signature'; import parseAcct from '../../../acct/parse'; import User, { IRemoteUser } from '../../../models/user'; -import act from '../../../remote/activitypub/act'; -import resolvePerson from '../../../remote/activitypub/resolve-person'; +import perform from '../../../remote/activitypub/perform'; +import { resolvePerson } from '../../../remote/activitypub/objects/person'; const log = debug('misskey:queue:inbox'); @@ -58,7 +58,7 @@ export default async (job: kue.Job, done): Promise => { // アクティビティを処理 try { - await act(user, activity); + await perform(user, activity); done(); } catch (e) { done(e); diff --git a/src/remote/activitypub/act/announce/index.ts b/src/remote/activitypub/act/announce/index.ts deleted file mode 100644 index c3ac06607d..0000000000 --- a/src/remote/activitypub/act/announce/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; -import announceNote from './note'; -import { IAnnounce } from '../../type'; - -const log = debug('misskey:activitypub'); - -export default async (actor: IRemoteUser, activity: IAnnounce): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - log(`Announce: ${uri}`); - - const resolver = new Resolver(); - - let object; - - try { - object = await resolver.resolve(activity.object); - } catch (e) { - log(`Resolution failed: ${e}`); - throw e; - } - - switch (object.type) { - case 'Note': - announceNote(resolver, actor, activity, object); - break; - - default: - console.warn(`Unknown announce type: ${object.type}`); - break; - } -}; diff --git a/src/remote/activitypub/act/announce/note.ts b/src/remote/activitypub/act/announce/note.ts deleted file mode 100644 index 24d159f184..0000000000 --- a/src/remote/activitypub/act/announce/note.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import Note from '../../../../models/note'; -import post from '../../../../services/note/create'; -import { IRemoteUser, isRemoteUser } from '../../../../models/user'; -import { IAnnounce, INote } from '../../type'; -import createNote from '../create/note'; -import resolvePerson from '../../resolve-person'; - -const log = debug('misskey:activitypub'); - -/** - * アナウンスアクティビティを捌きます - */ -export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise { - const uri = activity.id || activity; - - if (typeof uri !== 'string') { - throw new Error('invalid announce'); - } - - // 既に同じURIを持つものが登録されていないかチェック - const exist = await Note.findOne({ uri }); - if (exist) { - return; - } - - // アナウンス元の投稿の投稿者をフェッチ - const announcee = await resolvePerson(note.attributedTo); - - const renote = isRemoteUser(announcee) - ? await createNote(resolver, announcee, note, true) - : await Note.findOne({ _id: note.id.split('/').pop() }); - - log(`Creating the (Re)Note: ${uri}`); - - //#region Visibility - let visibility = 'public'; - if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; - if (activity.cc.length == 0) visibility = 'private'; - // TODO - if (visibility != 'public') throw new Error('unspported visibility'); - //#endergion - - await post(actor, { - createdAt: new Date(activity.published), - renote, - visibility, - uri - }); -} diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts deleted file mode 100644 index f1462f4ee4..0000000000 --- a/src/remote/activitypub/act/create/image.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as debug from 'debug'; - -import uploadFromUrl from '../../../../services/drive/upload-from-url'; -import { IRemoteUser } from '../../../../models/user'; -import { IDriveFile } from '../../../../models/drive-file'; - -const log = debug('misskey:activitypub'); - -export default async function(actor: IRemoteUser, image): Promise { - if ('attributedTo' in image && actor.uri !== image.attributedTo) { - log(`invalid image: ${JSON.stringify(image, null, 2)}`); - throw new Error('invalid image'); - } - - log(`Creating the Image: ${image.url}`); - - return await uploadFromUrl(image.url, actor); -} diff --git a/src/remote/activitypub/act/create/index.ts b/src/remote/activitypub/act/create/index.ts deleted file mode 100644 index 7cb9b08449..0000000000 --- a/src/remote/activitypub/act/create/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; -import createNote from './note'; -import createImage from './image'; -import { ICreate } from '../../type'; - -const log = debug('misskey:activitypub'); - -export default async (actor: IRemoteUser, activity: ICreate): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - log(`Create: ${uri}`); - - const resolver = new Resolver(); - - let object; - - try { - object = await resolver.resolve(activity.object); - } catch (e) { - log(`Resolution failed: ${e}`); - throw e; - } - - switch (object.type) { - case 'Image': - createImage(actor, object); - break; - - case 'Note': - createNote(resolver, actor, object); - break; - - default: - console.warn(`Unknown type: ${object.type}`); - break; - } -}; diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts deleted file mode 100644 index 599bc10aa8..0000000000 --- a/src/remote/activitypub/act/create/note.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { JSDOM } from 'jsdom'; -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import Note, { INote } from '../../../../models/note'; -import post from '../../../../services/note/create'; -import { IRemoteUser } from '../../../../models/user'; -import resolvePerson from '../../resolve-person'; -import createImage from './image'; -import config from '../../../../config'; - -const log = debug('misskey:activitypub'); - -/** - * 投稿作成アクティビティを捌きます - */ -export default async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise { - if (typeof note.id !== 'string') { - log(`invalid note: ${JSON.stringify(note, null, 2)}`); - throw new Error('invalid note'); - } - - // 既に同じURIを持つものが登録されていないかチェックし、登録されていたらそれを返す - const exist = await Note.findOne({ uri: note.id }); - if (exist) { - return exist; - } - - log(`Creating the Note: ${note.id}`); - - //#region Visibility - let visibility = 'public'; - if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; - if (note.cc.length == 0) visibility = 'private'; - // TODO - if (visibility != 'public') throw new Error('unspported visibility'); - //#endergion - - //#region 添付メディア - let media = []; - if ('attachment' in note && note.attachment != null) { - // TODO: attachmentは必ずしもImageではない - // TODO: attachmentは必ずしも配列ではない - media = await Promise.all(note.attachment.map(x => { - return createImage(actor, x); - })); - } - //#endregion - - //#region リプライ - let reply = null; - if ('inReplyTo' in note && note.inReplyTo != null) { - // リプライ先の投稿がMisskeyに登録されているか調べる - const uri: string = note.inReplyTo.id || note.inReplyTo; - const inReplyToNote = uri.startsWith(config.url + '/') - ? await Note.findOne({ _id: uri.split('/').pop() }) - : await Note.findOne({ uri }); - - if (inReplyToNote) { - reply = inReplyToNote; - } else { - // 無かったらフェッチ - const inReplyTo = await resolver.resolve(note.inReplyTo) as any; - - // リプライ先の投稿の投稿者をフェッチ - const actor = await resolvePerson(inReplyTo.attributedTo) as IRemoteUser; - - // TODO: silentを常にtrueにしてはならない - reply = await createNote(resolver, actor, inReplyTo); - } - } - //#endregion - - const { window } = new JSDOM(note.content); - - return await post(actor, { - createdAt: new Date(note.published), - media, - reply, - renote: undefined, - text: window.document.body.textContent, - viaMobile: false, - geo: undefined, - visibility, - uri: note.id - }); -} diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts deleted file mode 100644 index 10b47dc4ca..0000000000 --- a/src/remote/activitypub/act/delete/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Resolver from '../../resolver'; -import deleteNote from './note'; -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; - -/** - * 削除アクティビティを捌きます - */ -export default async (actor: IRemoteUser, activity): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object); - - const uri = (object as any).id; - - switch (object.type) { - case 'Note': - deleteNote(actor, uri); - break; - - case 'Tombstone': - const note = await Note.findOne({ uri }); - if (note != null) { - deleteNote(actor, uri); - } - break; - - default: - console.warn(`Unknown type: ${object.type}`); - break; - } -}; diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/act/delete/note.ts deleted file mode 100644 index 64c342d39b..0000000000 --- a/src/remote/activitypub/act/delete/note.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as debug from 'debug'; - -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; - -const log = debug('misskey:activitypub'); - -export default async function(actor: IRemoteUser, uri: string): Promise { - log(`Deleting the Note: ${uri}`); - - const note = await Note.findOne({ uri }); - - if (note == null) { - throw new Error('note not found'); - } - - if (!note.userId.equals(actor._id)) { - throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); - } - - Note.update({ _id: note._id }, { - $set: { - deletedAt: new Date(), - text: null, - textHtml: null, - mediaIds: [], - poll: null - } - }); -} diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts deleted file mode 100644 index 6a8b5a1bec..0000000000 --- a/src/remote/activitypub/act/follow.ts +++ /dev/null @@ -1,24 +0,0 @@ -import User, { IRemoteUser } from '../../../models/user'; -import config from '../../../config'; -import follow from '../../../services/following/create'; -import { IFollow } from '../type'; - -export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - - if (!id.startsWith(config.url + '/')) { - return null; - } - - const followee = await User.findOne({ _id: id.split('/').pop() }); - - if (followee === null) { - throw new Error('followee not found'); - } - - if (followee.host != null) { - throw new Error('フォローしようとしているユーザーはローカルユーザーではありません'); - } - - await follow(actor, followee, activity); -}; diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts deleted file mode 100644 index 15ea9494ae..0000000000 --- a/src/remote/activitypub/act/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Object } from '../type'; -import { IRemoteUser } from '../../../models/user'; -import create from './create'; -import performDeleteActivity from './delete'; -import follow from './follow'; -import undo from './undo'; -import like from './like'; -import announce from './announce'; - -const self = async (actor: IRemoteUser, activity: Object): Promise => { - switch (activity.type) { - case 'Create': - await create(actor, activity); - break; - - case 'Delete': - await performDeleteActivity(actor, activity); - break; - - case 'Follow': - await follow(actor, activity); - break; - - case 'Accept': - // noop - break; - - case 'Announce': - await announce(actor, activity); - break; - - case 'Like': - await like(actor, activity); - break; - - case 'Undo': - await undo(actor, activity); - break; - - case 'Collection': - case 'OrderedCollection': - // TODO - break; - - default: - console.warn(`unknown activity type: ${(activity as any).type}`); - return null; - } -}; - -export default self; diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/act/like.ts deleted file mode 100644 index 4941608588..0000000000 --- a/src/remote/activitypub/act/like.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Note from '../../../models/note'; -import { IRemoteUser } from '../../../models/user'; -import { ILike } from '../type'; -import create from '../../../services/note/reaction/create'; - -export default async (actor: IRemoteUser, activity: ILike) => { - const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - - // Transform: - // https://misskey.ex/notes/xxxx to - // xxxx - const noteId = id.split('/').pop(); - - const note = await Note.findOne({ _id: noteId }); - if (note === null) { - throw new Error(); - } - - await create(actor, note, 'pudding'); -}; diff --git a/src/remote/activitypub/act/undo/follow.ts b/src/remote/activitypub/act/undo/follow.ts deleted file mode 100644 index a85cb0305d..0000000000 --- a/src/remote/activitypub/act/undo/follow.ts +++ /dev/null @@ -1,24 +0,0 @@ -import User, { IRemoteUser } from '../../../../models/user'; -import config from '../../../../config'; -import unfollow from '../../../../services/following/delete'; -import { IFollow } from '../../type'; - -export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - - if (!id.startsWith(config.url + '/')) { - return null; - } - - const followee = await User.findOne({ _id: id.split('/').pop() }); - - if (followee === null) { - throw new Error('followee not found'); - } - - if (followee.host != null) { - throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); - } - - await unfollow(actor, followee, activity); -}; diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts deleted file mode 100644 index 71f547aeb9..0000000000 --- a/src/remote/activitypub/act/undo/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as debug from 'debug'; - -import { IRemoteUser } from '../../../../models/user'; -import { IUndo } from '../../type'; -import unfollow from './follow'; -import Resolver from '../../resolver'; - -const log = debug('misskey:activitypub'); - -export default async (actor: IRemoteUser, activity: IUndo): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - log(`Undo: ${uri}`); - - const resolver = new Resolver(); - - let object; - - try { - object = await resolver.resolve(activity.object); - } catch (e) { - log(`Resolution failed: ${e}`); - throw e; - } - - switch (object.type) { - case 'Follow': - unfollow(actor, object); - break; - } - - return null; -}; diff --git a/src/remote/activitypub/objects/image.ts b/src/remote/activitypub/objects/image.ts new file mode 100644 index 0000000000..7f79fc5c06 --- /dev/null +++ b/src/remote/activitypub/objects/image.ts @@ -0,0 +1,29 @@ +import * as debug from 'debug'; + +import uploadFromUrl from '../../../services/drive/upload-from-url'; +import { IRemoteUser } from '../../../models/user'; +import { IDriveFile } from '../../../models/drive-file'; + +const log = debug('misskey:activitypub'); + +/** + * Imageを作成します。 + */ +export async function createImage(actor: IRemoteUser, image): Promise { + log(`Creating the Image: ${image.url}`); + + return await uploadFromUrl(image.url, actor); +} + +/** + * Imageを解決します。 + * + * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolveImage(actor: IRemoteUser, value: any): Promise { + // TODO + + // リモートサーバーからフェッチしてきて登録 + return await createImage(actor, value); +} diff --git a/src/remote/activitypub/objects/note.ts b/src/remote/activitypub/objects/note.ts new file mode 100644 index 0000000000..3edcb8c63f --- /dev/null +++ b/src/remote/activitypub/objects/note.ts @@ -0,0 +1,110 @@ +import { JSDOM } from 'jsdom'; +import * as debug from 'debug'; + +import config from '../../../config'; +import Resolver from '../resolver'; +import Note, { INote } from '../../../models/note'; +import post from '../../../services/note/create'; +import { INote as INoteActivityStreamsObject, IObject } from '../type'; +import { resolvePerson } from './person'; +import { resolveImage } from './image'; +import { IRemoteUser } from '../../../models/user'; + +const log = debug('misskey:activitypub'); + +/** + * Noteをフェッチします。 + * + * Misskeyに対象のNoteが登録されていればそれを返します。 + */ +export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise { + const uri = typeof value == 'string' ? value : value.id; + + // URIがこのサーバーを指しているならデータベースからフェッチ + if (uri.startsWith(config.url + '/')) { + return await Note.findOne({ _id: uri.split('/').pop() }); + } + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await Note.findOne({ uri }); + + if (exist) { + return exist; + } + //#endregion + + return null; +} + +/** + * Noteを作成します。 + */ +export async function createNote(value: any, resolver?: Resolver, silent = false): Promise { + if (resolver == null) resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; + + if (object == null || object.type !== 'Note') { + throw new Error('invalid note'); + } + + const note: INoteActivityStreamsObject = object; + + log(`Creating the Note: ${note.id}`); + + // 投稿者をフェッチ + const actor = await resolvePerson(note.attributedTo) as IRemoteUser; + + //#region Visibility + let visibility = 'public'; + if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; + if (note.cc.length == 0) visibility = 'private'; + // TODO + if (visibility != 'public') throw new Error('unspported visibility'); + //#endergion + + // 添付メディア + // TODO: attachmentは必ずしもImageではない + // TODO: attachmentは必ずしも配列ではない + const media = note.attachment + ? await Promise.all(note.attachment.map(x => resolveImage(actor, x))) + : []; + + // リプライ + const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null; + + const { window } = new JSDOM(note.content); + + return await post(actor, { + createdAt: new Date(note.published), + media, + reply, + renote: undefined, + text: window.document.body.textContent, + viaMobile: false, + geo: undefined, + visibility, + uri: note.id + }, silent); +} + +/** + * Noteを解決します。 + * + * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise { + const uri = typeof value == 'string' ? value : value.id; + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await fetchNote(uri); + + if (exist) { + return exist; + } + //#endregion + + // リモートサーバーからフェッチしてきて登録 + return await createNote(value, resolver); +} diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts new file mode 100644 index 0000000000..b1e8c9ee0a --- /dev/null +++ b/src/remote/activitypub/objects/person.ts @@ -0,0 +1,142 @@ +import { JSDOM } from 'jsdom'; +import { toUnicode } from 'punycode'; +import * as debug from 'debug'; + +import config from '../../../config'; +import User, { validateUsername, isValidName, isValidDescription, IUser, IRemoteUser } from '../../../models/user'; +import webFinger from '../../webfinger'; +import Resolver from '../resolver'; +import { resolveImage } from './image'; +import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type'; + +const log = debug('misskey:activitypub'); + +/** + * Personをフェッチします。 + * + * Misskeyに対象のPersonが登録されていればそれを返します。 + */ +export async function fetchPerson(value: string | IObject, resolver?: Resolver): Promise { + const uri = typeof value == 'string' ? value : value.id; + + // URIがこのサーバーを指しているならデータベースからフェッチ + if (uri.startsWith(config.url + '/')) { + return await User.findOne({ _id: uri.split('/').pop() }); + } + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await User.findOne({ uri }); + + if (exist) { + return exist; + } + //#endregion + + return null; +} + +/** + * Personを作成します。 + */ +export async function createPerson(value: any, resolver?: Resolver): Promise { + if (resolver == null) resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; + + if ( + object == null || + object.type !== 'Person' || + typeof object.preferredUsername !== 'string' || + !validateUsername(object.preferredUsername) || + !isValidName(object.name == '' ? null : object.name) || + !isValidDescription(object.summary) + ) { + throw new Error('invalid person'); + } + + const person: IPerson = object; + + log(`Creating the Person: ${person.id}`); + + const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([ + resolver.resolve(person.followers).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined + ), + resolver.resolve(person.following).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined + ), + resolver.resolve(person.outbox).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined + ), + webFinger(person.id) + ]); + + const host = toUnicode(finger.subject.replace(/^.*?@/, '')); + const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase()); + const summaryDOM = JSDOM.fragment(person.summary); + + // Create user + const user = await User.insert({ + avatarId: null, + bannerId: null, + createdAt: Date.parse(person.published) || null, + description: summaryDOM.textContent, + followersCount, + followingCount, + notesCount, + name: person.name, + driveCapacity: 1024 * 1024 * 8, // 8MiB + username: person.preferredUsername, + usernameLower: person.preferredUsername.toLowerCase(), + host, + hostLower, + publicKey: { + id: person.publicKey.id, + publicKeyPem: person.publicKey.publicKeyPem + }, + inbox: person.inbox, + uri: person.id + }) as IRemoteUser; + + //#region アイコンとヘッダー画像をフェッチ + const [avatarId, bannerId] = (await Promise.all([ + person.icon, + person.image + ].map(img => + img == null + ? Promise.resolve(null) + : resolveImage(user, img.url) + ))).map(file => file != null ? file._id : null); + + User.update({ _id: user._id }, { $set: { avatarId, bannerId } }); + + user.avatarId = avatarId; + user.bannerId = bannerId; + //#endregion + + return user; +} + +/** + * Personを解決します。 + * + * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolvePerson(value: string | IObject, verifier?: string): Promise { + const uri = typeof value == 'string' ? value : value.id; + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await fetchPerson(uri); + + if (exist) { + return exist; + } + //#endregion + + // リモートサーバーからフェッチしてきて登録 + return await createPerson(value); +} diff --git a/src/remote/activitypub/perform/announce/index.ts b/src/remote/activitypub/perform/announce/index.ts new file mode 100644 index 0000000000..c3ac06607d --- /dev/null +++ b/src/remote/activitypub/perform/announce/index.ts @@ -0,0 +1,39 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import announceNote from './note'; +import { IAnnounce } from '../../type'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IAnnounce): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Announce: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Note': + announceNote(resolver, actor, activity, object); + break; + + default: + console.warn(`Unknown announce type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/perform/announce/note.ts b/src/remote/activitypub/perform/announce/note.ts new file mode 100644 index 0000000000..68fb23c97f --- /dev/null +++ b/src/remote/activitypub/perform/announce/note.ts @@ -0,0 +1,45 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import post from '../../../../services/note/create'; +import { IRemoteUser } from '../../../../models/user'; +import { IAnnounce, INote } from '../../type'; +import { fetchNote, resolveNote } from '../../objects/note'; + +const log = debug('misskey:activitypub'); + +/** + * アナウンスアクティビティを捌きます + */ +export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise { + const uri = activity.id || activity; + + if (typeof uri !== 'string') { + throw new Error('invalid announce'); + } + + // 既に同じURIを持つものが登録されていないかチェック + const exist = await fetchNote(uri); + if (exist) { + return; + } + + const renote = await resolveNote(note); + + log(`Creating the (Re)Note: ${uri}`); + + //#region Visibility + let visibility = 'public'; + if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; + if (activity.cc.length == 0) visibility = 'private'; + // TODO + if (visibility != 'public') throw new Error('unspported visibility'); + //#endergion + + await post(actor, { + createdAt: new Date(activity.published), + renote, + visibility, + uri + }); +} diff --git a/src/remote/activitypub/perform/create/image.ts b/src/remote/activitypub/perform/create/image.ts new file mode 100644 index 0000000000..ea36545f0c --- /dev/null +++ b/src/remote/activitypub/perform/create/image.ts @@ -0,0 +1,6 @@ +import { IRemoteUser } from '../../../../models/user'; +import { createImage } from '../../objects/image'; + +export default async function(actor: IRemoteUser, image): Promise { + await createImage(image.url, actor); +} diff --git a/src/remote/activitypub/perform/create/index.ts b/src/remote/activitypub/perform/create/index.ts new file mode 100644 index 0000000000..7cb9b08449 --- /dev/null +++ b/src/remote/activitypub/perform/create/index.ts @@ -0,0 +1,44 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import createNote from './note'; +import createImage from './image'; +import { ICreate } from '../../type'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: ICreate): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Create: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Image': + createImage(actor, object); + break; + + case 'Note': + createNote(resolver, actor, object); + break; + + default: + console.warn(`Unknown type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/perform/create/note.ts b/src/remote/activitypub/perform/create/note.ts new file mode 100644 index 0000000000..530cf6483f --- /dev/null +++ b/src/remote/activitypub/perform/create/note.ts @@ -0,0 +1,13 @@ +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import { createNote, fetchNote } from '../../objects/note'; + +/** + * 投稿作成アクティビティを捌きます + */ +export default async function(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise { + const exist = await fetchNote(note); + if (exist == null) { + await createNote(note); + } +} diff --git a/src/remote/activitypub/perform/delete/index.ts b/src/remote/activitypub/perform/delete/index.ts new file mode 100644 index 0000000000..10b47dc4ca --- /dev/null +++ b/src/remote/activitypub/perform/delete/index.ts @@ -0,0 +1,36 @@ +import Resolver from '../../resolver'; +import deleteNote from './note'; +import Note from '../../../../models/note'; +import { IRemoteUser } from '../../../../models/user'; + +/** + * 削除アクティビティを捌きます + */ +export default async (actor: IRemoteUser, activity): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const resolver = new Resolver(); + + const object = await resolver.resolve(activity.object); + + const uri = (object as any).id; + + switch (object.type) { + case 'Note': + deleteNote(actor, uri); + break; + + case 'Tombstone': + const note = await Note.findOne({ uri }); + if (note != null) { + deleteNote(actor, uri); + } + break; + + default: + console.warn(`Unknown type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/perform/delete/note.ts b/src/remote/activitypub/perform/delete/note.ts new file mode 100644 index 0000000000..64c342d39b --- /dev/null +++ b/src/remote/activitypub/perform/delete/note.ts @@ -0,0 +1,30 @@ +import * as debug from 'debug'; + +import Note from '../../../../models/note'; +import { IRemoteUser } from '../../../../models/user'; + +const log = debug('misskey:activitypub'); + +export default async function(actor: IRemoteUser, uri: string): Promise { + log(`Deleting the Note: ${uri}`); + + const note = await Note.findOne({ uri }); + + if (note == null) { + throw new Error('note not found'); + } + + if (!note.userId.equals(actor._id)) { + throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); + } + + Note.update({ _id: note._id }, { + $set: { + deletedAt: new Date(), + text: null, + textHtml: null, + mediaIds: [], + poll: null + } + }); +} diff --git a/src/remote/activitypub/perform/follow.ts b/src/remote/activitypub/perform/follow.ts new file mode 100644 index 0000000000..6a8b5a1bec --- /dev/null +++ b/src/remote/activitypub/perform/follow.ts @@ -0,0 +1,24 @@ +import User, { IRemoteUser } from '../../../models/user'; +import config from '../../../config'; +import follow from '../../../services/following/create'; +import { IFollow } from '../type'; + +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + if (!id.startsWith(config.url + '/')) { + return null; + } + + const followee = await User.findOne({ _id: id.split('/').pop() }); + + if (followee === null) { + throw new Error('followee not found'); + } + + if (followee.host != null) { + throw new Error('フォローしようとしているユーザーはローカルユーザーではありません'); + } + + await follow(actor, followee, activity); +}; diff --git a/src/remote/activitypub/perform/index.ts b/src/remote/activitypub/perform/index.ts new file mode 100644 index 0000000000..15ea9494ae --- /dev/null +++ b/src/remote/activitypub/perform/index.ts @@ -0,0 +1,51 @@ +import { Object } from '../type'; +import { IRemoteUser } from '../../../models/user'; +import create from './create'; +import performDeleteActivity from './delete'; +import follow from './follow'; +import undo from './undo'; +import like from './like'; +import announce from './announce'; + +const self = async (actor: IRemoteUser, activity: Object): Promise => { + switch (activity.type) { + case 'Create': + await create(actor, activity); + break; + + case 'Delete': + await performDeleteActivity(actor, activity); + break; + + case 'Follow': + await follow(actor, activity); + break; + + case 'Accept': + // noop + break; + + case 'Announce': + await announce(actor, activity); + break; + + case 'Like': + await like(actor, activity); + break; + + case 'Undo': + await undo(actor, activity); + break; + + case 'Collection': + case 'OrderedCollection': + // TODO + break; + + default: + console.warn(`unknown activity type: ${(activity as any).type}`); + return null; + } +}; + +export default self; diff --git a/src/remote/activitypub/perform/like.ts b/src/remote/activitypub/perform/like.ts new file mode 100644 index 0000000000..4941608588 --- /dev/null +++ b/src/remote/activitypub/perform/like.ts @@ -0,0 +1,20 @@ +import Note from '../../../models/note'; +import { IRemoteUser } from '../../../models/user'; +import { ILike } from '../type'; +import create from '../../../services/note/reaction/create'; + +export default async (actor: IRemoteUser, activity: ILike) => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + // Transform: + // https://misskey.ex/notes/xxxx to + // xxxx + const noteId = id.split('/').pop(); + + const note = await Note.findOne({ _id: noteId }); + if (note === null) { + throw new Error(); + } + + await create(actor, note, 'pudding'); +}; diff --git a/src/remote/activitypub/perform/undo/follow.ts b/src/remote/activitypub/perform/undo/follow.ts new file mode 100644 index 0000000000..a85cb0305d --- /dev/null +++ b/src/remote/activitypub/perform/undo/follow.ts @@ -0,0 +1,24 @@ +import User, { IRemoteUser } from '../../../../models/user'; +import config from '../../../../config'; +import unfollow from '../../../../services/following/delete'; +import { IFollow } from '../../type'; + +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + if (!id.startsWith(config.url + '/')) { + return null; + } + + const followee = await User.findOne({ _id: id.split('/').pop() }); + + if (followee === null) { + throw new Error('followee not found'); + } + + if (followee.host != null) { + throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); + } + + await unfollow(actor, followee, activity); +}; diff --git a/src/remote/activitypub/perform/undo/index.ts b/src/remote/activitypub/perform/undo/index.ts new file mode 100644 index 0000000000..71f547aeb9 --- /dev/null +++ b/src/remote/activitypub/perform/undo/index.ts @@ -0,0 +1,37 @@ +import * as debug from 'debug'; + +import { IRemoteUser } from '../../../../models/user'; +import { IUndo } from '../../type'; +import unfollow from './follow'; +import Resolver from '../../resolver'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IUndo): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Undo: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Follow': + unfollow(actor, object); + break; + } + + return null; +}; diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts deleted file mode 100644 index 50e7873cbd..0000000000 --- a/src/remote/activitypub/resolve-person.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { JSDOM } from 'jsdom'; -import { toUnicode } from 'punycode'; -import config from '../../config'; -import User, { validateUsername, isValidName, isValidDescription, IUser } from '../../models/user'; -import webFinger from '../webfinger'; -import Resolver from './resolver'; -import uploadFromUrl from '../../services/drive/upload-from-url'; -import { isCollectionOrOrderedCollection, IObject } from './type'; - -export default async (value: string | IObject, verifier?: string): Promise => { - const id = typeof value == 'string' ? value : value.id; - - if (id.startsWith(config.url + '/')) { - return await User.findOne({ _id: id.split('/').pop() }); - } else { - const exist = await User.findOne({ - uri: id - }); - - if (exist) { - return exist; - } - } - - const resolver = new Resolver(); - - const object = await resolver.resolve(value) as any; - - if ( - object == null || - object.type !== 'Person' || - typeof object.preferredUsername !== 'string' || - !validateUsername(object.preferredUsername) || - !isValidName(object.name == '' ? null : object.name) || - !isValidDescription(object.summary) - ) { - throw new Error('invalid person'); - } - - const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([ - resolver.resolve(object.followers).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(object.following).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(object.outbox).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - webFinger(id, verifier) - ]); - - const host = toUnicode(finger.subject.replace(/^.*?@/, '')); - const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase()); - const summaryDOM = JSDOM.fragment(object.summary); - - // Create user - const user = await User.insert({ - avatarId: null, - bannerId: null, - createdAt: Date.parse(object.published) || null, - description: summaryDOM.textContent, - followersCount, - followingCount, - notesCount, - name: object.name, - driveCapacity: 1024 * 1024 * 8, // 8MiB - username: object.preferredUsername, - usernameLower: object.preferredUsername.toLowerCase(), - host, - hostLower, - publicKey: { - id: object.publicKey.id, - publicKeyPem: object.publicKey.publicKeyPem - }, - inbox: object.inbox, - uri: id - }); - - const [avatarId, bannerId] = (await Promise.all([ - object.icon, - object.image - ].map(img => - img == null - ? Promise.resolve(null) - : uploadFromUrl(img.url, user) - ))).map(file => file != null ? file._id : null); - - User.update({ _id: user._id }, { $set: { avatarId, bannerId } }); - - user.avatarId = avatarId; - user.bannerId = bannerId; - - return user; -}; diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 2335517645..983eb621fa 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -9,6 +9,11 @@ export interface IObject { cc?: string[]; to?: string[]; attributedTo: string; + attachment?: any[]; + inReplyTo?: any; + content: string; + icon?: any; + image?: any; } export interface IActivity extends IObject { @@ -34,6 +39,17 @@ export interface INote extends IObject { type: 'Note'; } +export interface IPerson extends IObject { + type: 'Person'; + name: string; + preferredUsername: string; + inbox: string; + publicKey: any; + followers: any; + following: any; + outbox: any; +} + export const isCollection = (object: IObject): object is ICollection => object.type === 'Collection'; diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts index 0e7edd8e12..346e134c9f 100644 --- a/src/remote/resolve-user.ts +++ b/src/remote/resolve-user.ts @@ -1,8 +1,8 @@ import { toUnicode, toASCII } from 'punycode'; import User from '../models/user'; -import resolvePerson from './activitypub/resolve-person'; import webFinger from './webfinger'; import config from '../config'; +import { createPerson } from './activitypub/objects/person'; export default async (username, host, option) => { const usernameLower = username.toLowerCase(); @@ -18,13 +18,13 @@ export default async (username, host, option) => { if (user === null) { const acctLower = `${usernameLower}@${hostLowerAscii}`; - const finger = await webFinger(acctLower, acctLower); + const finger = await webFinger(acctLower); const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); if (!self) { throw new Error('self link not found'); } - user = await resolvePerson(self.href, acctLower); + user = await createPerson(self.href); } return user; diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts index bfca8d1c86..4f1ff231c0 100644 --- a/src/remote/webfinger.ts +++ b/src/remote/webfinger.ts @@ -3,36 +3,21 @@ const WebFinger = require('webfinger.js'); const webFinger = new WebFinger({ }); type ILink = { - href: string; - rel: string; + href: string; + rel: string; }; type IWebFinger = { - links: ILink[]; - subject: string; + links: ILink[]; + subject: string; }; -export default async function resolve(query, verifier?: string): Promise { - const finger = await new Promise((res, rej) => webFinger.lookup(query, (error, result) => { +export default async function resolve(query): Promise { + return await new Promise((res, rej) => webFinger.lookup(query, (error, result) => { if (error) { return rej(error); } res(result.object); })) as IWebFinger; - const subject = finger.subject.toLowerCase().replace(/^acct:/, ''); - - if (typeof verifier === 'string') { - if (subject !== verifier) { - throw new Error(); - } - - return finger; - } - - if (typeof subject === 'string') { - return resolve(subject, subject); - } - - throw new Error(); } -- cgit v1.2.3-freya From 536277122d6c8f951114df6f7867095639343991 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 9 Apr 2018 05:02:52 +0900 Subject: wip --- src/remote/activitypub/kernel/announce/index.ts | 39 ++++++++++++++++++ src/remote/activitypub/kernel/announce/note.ts | 45 +++++++++++++++++++++ src/remote/activitypub/kernel/create/image.ts | 6 +++ src/remote/activitypub/kernel/create/index.ts | 44 ++++++++++++++++++++ src/remote/activitypub/kernel/create/note.ts | 13 ++++++ src/remote/activitypub/kernel/delete/index.ts | 36 +++++++++++++++++ src/remote/activitypub/kernel/delete/note.ts | 30 ++++++++++++++ src/remote/activitypub/kernel/follow.ts | 24 +++++++++++ src/remote/activitypub/kernel/index.ts | 51 ++++++++++++++++++++++++ src/remote/activitypub/kernel/like.ts | 20 ++++++++++ src/remote/activitypub/kernel/undo/follow.ts | 24 +++++++++++ src/remote/activitypub/kernel/undo/index.ts | 37 +++++++++++++++++ src/remote/activitypub/perform.ts | 7 ++++ src/remote/activitypub/perform/announce/index.ts | 39 ------------------ src/remote/activitypub/perform/announce/note.ts | 45 --------------------- src/remote/activitypub/perform/create/image.ts | 6 --- src/remote/activitypub/perform/create/index.ts | 44 -------------------- src/remote/activitypub/perform/create/note.ts | 13 ------ src/remote/activitypub/perform/delete/index.ts | 36 ----------------- src/remote/activitypub/perform/delete/note.ts | 30 -------------- src/remote/activitypub/perform/follow.ts | 24 ----------- src/remote/activitypub/perform/index.ts | 51 ------------------------ src/remote/activitypub/perform/like.ts | 20 ---------- src/remote/activitypub/perform/undo/follow.ts | 24 ----------- src/remote/activitypub/perform/undo/index.ts | 37 ----------------- 25 files changed, 376 insertions(+), 369 deletions(-) create mode 100644 src/remote/activitypub/kernel/announce/index.ts create mode 100644 src/remote/activitypub/kernel/announce/note.ts create mode 100644 src/remote/activitypub/kernel/create/image.ts create mode 100644 src/remote/activitypub/kernel/create/index.ts create mode 100644 src/remote/activitypub/kernel/create/note.ts create mode 100644 src/remote/activitypub/kernel/delete/index.ts create mode 100644 src/remote/activitypub/kernel/delete/note.ts create mode 100644 src/remote/activitypub/kernel/follow.ts create mode 100644 src/remote/activitypub/kernel/index.ts create mode 100644 src/remote/activitypub/kernel/like.ts create mode 100644 src/remote/activitypub/kernel/undo/follow.ts create mode 100644 src/remote/activitypub/kernel/undo/index.ts create mode 100644 src/remote/activitypub/perform.ts delete mode 100644 src/remote/activitypub/perform/announce/index.ts delete mode 100644 src/remote/activitypub/perform/announce/note.ts delete mode 100644 src/remote/activitypub/perform/create/image.ts delete mode 100644 src/remote/activitypub/perform/create/index.ts delete mode 100644 src/remote/activitypub/perform/create/note.ts delete mode 100644 src/remote/activitypub/perform/delete/index.ts delete mode 100644 src/remote/activitypub/perform/delete/note.ts delete mode 100644 src/remote/activitypub/perform/follow.ts delete mode 100644 src/remote/activitypub/perform/index.ts delete mode 100644 src/remote/activitypub/perform/like.ts delete mode 100644 src/remote/activitypub/perform/undo/follow.ts delete mode 100644 src/remote/activitypub/perform/undo/index.ts (limited to 'src/remote') diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts new file mode 100644 index 0000000000..c3ac06607d --- /dev/null +++ b/src/remote/activitypub/kernel/announce/index.ts @@ -0,0 +1,39 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import announceNote from './note'; +import { IAnnounce } from '../../type'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IAnnounce): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Announce: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Note': + announceNote(resolver, actor, activity, object); + break; + + default: + console.warn(`Unknown announce type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/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 { + 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 { + 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..7cb9b08449 --- /dev/null +++ b/src/remote/activitypub/kernel/create/index.ts @@ -0,0 +1,44 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import createNote from './note'; +import createImage from './image'; +import { ICreate } from '../../type'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: ICreate): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Create: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Image': + createImage(actor, object); + break; + + case 'Note': + createNote(resolver, actor, object); + break; + + default: + console.warn(`Unknown type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/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 { + 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 => { + 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 { + 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 => { + 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 => { + 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 => { + 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 => { + 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/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 => { + await kernel(actor, activity); +}; diff --git a/src/remote/activitypub/perform/announce/index.ts b/src/remote/activitypub/perform/announce/index.ts deleted file mode 100644 index c3ac06607d..0000000000 --- a/src/remote/activitypub/perform/announce/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; -import announceNote from './note'; -import { IAnnounce } from '../../type'; - -const log = debug('misskey:activitypub'); - -export default async (actor: IRemoteUser, activity: IAnnounce): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - log(`Announce: ${uri}`); - - const resolver = new Resolver(); - - let object; - - try { - object = await resolver.resolve(activity.object); - } catch (e) { - log(`Resolution failed: ${e}`); - throw e; - } - - switch (object.type) { - case 'Note': - announceNote(resolver, actor, activity, object); - break; - - default: - console.warn(`Unknown announce type: ${object.type}`); - break; - } -}; diff --git a/src/remote/activitypub/perform/announce/note.ts b/src/remote/activitypub/perform/announce/note.ts deleted file mode 100644 index 68fb23c97f..0000000000 --- a/src/remote/activitypub/perform/announce/note.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import post from '../../../../services/note/create'; -import { IRemoteUser } from '../../../../models/user'; -import { IAnnounce, INote } from '../../type'; -import { fetchNote, resolveNote } from '../../objects/note'; - -const log = debug('misskey:activitypub'); - -/** - * アナウンスアクティビティを捌きます - */ -export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise { - const uri = activity.id || activity; - - if (typeof uri !== 'string') { - throw new Error('invalid announce'); - } - - // 既に同じURIを持つものが登録されていないかチェック - const exist = await fetchNote(uri); - if (exist) { - return; - } - - const renote = await resolveNote(note); - - log(`Creating the (Re)Note: ${uri}`); - - //#region Visibility - let visibility = 'public'; - if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; - if (activity.cc.length == 0) visibility = 'private'; - // TODO - if (visibility != 'public') throw new Error('unspported visibility'); - //#endergion - - await post(actor, { - createdAt: new Date(activity.published), - renote, - visibility, - uri - }); -} diff --git a/src/remote/activitypub/perform/create/image.ts b/src/remote/activitypub/perform/create/image.ts deleted file mode 100644 index ea36545f0c..0000000000 --- a/src/remote/activitypub/perform/create/image.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IRemoteUser } from '../../../../models/user'; -import { createImage } from '../../objects/image'; - -export default async function(actor: IRemoteUser, image): Promise { - await createImage(image.url, actor); -} diff --git a/src/remote/activitypub/perform/create/index.ts b/src/remote/activitypub/perform/create/index.ts deleted file mode 100644 index 7cb9b08449..0000000000 --- a/src/remote/activitypub/perform/create/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as debug from 'debug'; - -import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; -import createNote from './note'; -import createImage from './image'; -import { ICreate } from '../../type'; - -const log = debug('misskey:activitypub'); - -export default async (actor: IRemoteUser, activity: ICreate): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - log(`Create: ${uri}`); - - const resolver = new Resolver(); - - let object; - - try { - object = await resolver.resolve(activity.object); - } catch (e) { - log(`Resolution failed: ${e}`); - throw e; - } - - switch (object.type) { - case 'Image': - createImage(actor, object); - break; - - case 'Note': - createNote(resolver, actor, object); - break; - - default: - console.warn(`Unknown type: ${object.type}`); - break; - } -}; diff --git a/src/remote/activitypub/perform/create/note.ts b/src/remote/activitypub/perform/create/note.ts deleted file mode 100644 index 530cf6483f..0000000000 --- a/src/remote/activitypub/perform/create/note.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; -import { createNote, fetchNote } from '../../objects/note'; - -/** - * 投稿作成アクティビティを捌きます - */ -export default async function(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise { - const exist = await fetchNote(note); - if (exist == null) { - await createNote(note); - } -} diff --git a/src/remote/activitypub/perform/delete/index.ts b/src/remote/activitypub/perform/delete/index.ts deleted file mode 100644 index 10b47dc4ca..0000000000 --- a/src/remote/activitypub/perform/delete/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Resolver from '../../resolver'; -import deleteNote from './note'; -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; - -/** - * 削除アクティビティを捌きます - */ -export default async (actor: IRemoteUser, activity): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object); - - const uri = (object as any).id; - - switch (object.type) { - case 'Note': - deleteNote(actor, uri); - break; - - case 'Tombstone': - const note = await Note.findOne({ uri }); - if (note != null) { - deleteNote(actor, uri); - } - break; - - default: - console.warn(`Unknown type: ${object.type}`); - break; - } -}; diff --git a/src/remote/activitypub/perform/delete/note.ts b/src/remote/activitypub/perform/delete/note.ts deleted file mode 100644 index 64c342d39b..0000000000 --- a/src/remote/activitypub/perform/delete/note.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as debug from 'debug'; - -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; - -const log = debug('misskey:activitypub'); - -export default async function(actor: IRemoteUser, uri: string): Promise { - log(`Deleting the Note: ${uri}`); - - const note = await Note.findOne({ uri }); - - if (note == null) { - throw new Error('note not found'); - } - - if (!note.userId.equals(actor._id)) { - throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); - } - - Note.update({ _id: note._id }, { - $set: { - deletedAt: new Date(), - text: null, - textHtml: null, - mediaIds: [], - poll: null - } - }); -} diff --git a/src/remote/activitypub/perform/follow.ts b/src/remote/activitypub/perform/follow.ts deleted file mode 100644 index 6a8b5a1bec..0000000000 --- a/src/remote/activitypub/perform/follow.ts +++ /dev/null @@ -1,24 +0,0 @@ -import User, { IRemoteUser } from '../../../models/user'; -import config from '../../../config'; -import follow from '../../../services/following/create'; -import { IFollow } from '../type'; - -export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - - if (!id.startsWith(config.url + '/')) { - return null; - } - - const followee = await User.findOne({ _id: id.split('/').pop() }); - - if (followee === null) { - throw new Error('followee not found'); - } - - if (followee.host != null) { - throw new Error('フォローしようとしているユーザーはローカルユーザーではありません'); - } - - await follow(actor, followee, activity); -}; diff --git a/src/remote/activitypub/perform/index.ts b/src/remote/activitypub/perform/index.ts deleted file mode 100644 index 15ea9494ae..0000000000 --- a/src/remote/activitypub/perform/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Object } from '../type'; -import { IRemoteUser } from '../../../models/user'; -import create from './create'; -import performDeleteActivity from './delete'; -import follow from './follow'; -import undo from './undo'; -import like from './like'; -import announce from './announce'; - -const self = async (actor: IRemoteUser, activity: Object): Promise => { - switch (activity.type) { - case 'Create': - await create(actor, activity); - break; - - case 'Delete': - await performDeleteActivity(actor, activity); - break; - - case 'Follow': - await follow(actor, activity); - break; - - case 'Accept': - // noop - break; - - case 'Announce': - await announce(actor, activity); - break; - - case 'Like': - await like(actor, activity); - break; - - case 'Undo': - await undo(actor, activity); - break; - - case 'Collection': - case 'OrderedCollection': - // TODO - break; - - default: - console.warn(`unknown activity type: ${(activity as any).type}`); - return null; - } -}; - -export default self; diff --git a/src/remote/activitypub/perform/like.ts b/src/remote/activitypub/perform/like.ts deleted file mode 100644 index 4941608588..0000000000 --- a/src/remote/activitypub/perform/like.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Note from '../../../models/note'; -import { IRemoteUser } from '../../../models/user'; -import { ILike } from '../type'; -import create from '../../../services/note/reaction/create'; - -export default async (actor: IRemoteUser, activity: ILike) => { - const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - - // Transform: - // https://misskey.ex/notes/xxxx to - // xxxx - const noteId = id.split('/').pop(); - - const note = await Note.findOne({ _id: noteId }); - if (note === null) { - throw new Error(); - } - - await create(actor, note, 'pudding'); -}; diff --git a/src/remote/activitypub/perform/undo/follow.ts b/src/remote/activitypub/perform/undo/follow.ts deleted file mode 100644 index a85cb0305d..0000000000 --- a/src/remote/activitypub/perform/undo/follow.ts +++ /dev/null @@ -1,24 +0,0 @@ -import User, { IRemoteUser } from '../../../../models/user'; -import config from '../../../../config'; -import unfollow from '../../../../services/following/delete'; -import { IFollow } from '../../type'; - -export default async (actor: IRemoteUser, activity: IFollow): Promise => { - const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - - if (!id.startsWith(config.url + '/')) { - return null; - } - - const followee = await User.findOne({ _id: id.split('/').pop() }); - - if (followee === null) { - throw new Error('followee not found'); - } - - if (followee.host != null) { - throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); - } - - await unfollow(actor, followee, activity); -}; diff --git a/src/remote/activitypub/perform/undo/index.ts b/src/remote/activitypub/perform/undo/index.ts deleted file mode 100644 index 71f547aeb9..0000000000 --- a/src/remote/activitypub/perform/undo/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as debug from 'debug'; - -import { IRemoteUser } from '../../../../models/user'; -import { IUndo } from '../../type'; -import unfollow from './follow'; -import Resolver from '../../resolver'; - -const log = debug('misskey:activitypub'); - -export default async (actor: IRemoteUser, activity: IUndo): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - log(`Undo: ${uri}`); - - const resolver = new Resolver(); - - let object; - - try { - object = await resolver.resolve(activity.object); - } catch (e) { - log(`Resolution failed: ${e}`); - throw e; - } - - switch (object.type) { - case 'Follow': - unfollow(actor, object); - break; - } - - return null; -}; -- cgit v1.2.3-freya From d8a87379ca512a01ad0177697b830ca0ce8dc104 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 9 Apr 2018 05:14:47 +0900 Subject: Fix bug --- src/remote/activitypub/objects/image.ts | 9 ++++++++- src/remote/activitypub/type.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'src/remote') diff --git a/src/remote/activitypub/objects/image.ts b/src/remote/activitypub/objects/image.ts index 7f79fc5c06..d7bc5aff2f 100644 --- a/src/remote/activitypub/objects/image.ts +++ b/src/remote/activitypub/objects/image.ts @@ -3,13 +3,20 @@ 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, image): Promise { +export async function createImage(actor: IRemoteUser, value): Promise { + 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); diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 983eb621fa..08e5493dd4 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -14,6 +14,7 @@ export interface IObject { content: string; icon?: any; image?: any; + url?: string; } export interface IActivity extends IObject { -- cgit v1.2.3-freya From cf5b6791177767816708e9e96bbd91d1c8e2a8bb Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 9 Apr 2018 05:23:35 +0900 Subject: :v: --- src/remote/activitypub/kernel/create/index.ts | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src/remote') diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts index 7cb9b08449..e11bcac811 100644 --- a/src/remote/activitypub/kernel/create/index.ts +++ b/src/remote/activitypub/kernel/create/index.ts @@ -9,10 +9,6 @@ import { ICreate } from '../../type'; const log = debug('misskey:activitypub'); export default async (actor: IRemoteUser, activity: ICreate): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - const uri = activity.id || activity; log(`Create: ${uri}`); -- cgit v1.2.3-freya From 1186b788d65b027bf00cd78c10140a1afb131145 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 9 Apr 2018 05:27:38 +0900 Subject: :v: --- src/remote/activitypub/resolver.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) (limited to 'src/remote') diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 7d45783b47..f405ff10c3 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -48,11 +48,6 @@ export default class Resolver { this.history.add(value); - //#region resolve local objects - // TODO - //if (value.startsWith(`${config.url}/`)) { - //#endregion - const object = await request({ url: value, headers: { @@ -66,12 +61,10 @@ export default class Resolver { !object['@context'].includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' )) { - log(`invalid response: ${JSON.stringify(object, null, 2)}`); + log(`invalid response: ${value}`); throw new Error('invalid response'); } - log(`resolved: ${JSON.stringify(object, null, 2)}`); - return object; } } -- cgit v1.2.3-freya From 44e5ae110e1387565a7e9628478283a1d82b4e8c Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 9 Apr 2018 05:30:17 +0900 Subject: :v: --- src/remote/activitypub/objects/person.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'src/remote') diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts index b1e8c9ee0a..151b4a7a79 100644 --- a/src/remote/activitypub/objects/person.ts +++ b/src/remote/activitypub/objects/person.ts @@ -51,6 +51,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise Date: Mon, 9 Apr 2018 05:34:33 +0900 Subject: :v: --- src/remote/activitypub/kernel/announce/index.ts | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src/remote') diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts index c3ac06607d..a2cf2d5762 100644 --- a/src/remote/activitypub/kernel/announce/index.ts +++ b/src/remote/activitypub/kernel/announce/index.ts @@ -8,10 +8,6 @@ import { IAnnounce } from '../../type'; const log = debug('misskey:activitypub'); export default async (actor: IRemoteUser, activity: IAnnounce): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - const uri = activity.id || activity; log(`Announce: ${uri}`); -- cgit v1.2.3-freya From 94e48ba122854a89ce2a2f8979b9b62c0d3f6817 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 9 Apr 2018 05:37:50 +0900 Subject: :v: --- src/remote/activitypub/objects/person.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/remote') diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts index 151b4a7a79..c5119fb853 100644 --- a/src/remote/activitypub/objects/person.ts +++ b/src/remote/activitypub/objects/person.ts @@ -48,8 +48,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise Date: Mon, 9 Apr 2018 05:39:18 +0900 Subject: oops --- src/remote/activitypub/objects/person.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/remote') diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts index c5119fb853..a78538c4bd 100644 --- a/src/remote/activitypub/objects/person.ts +++ b/src/remote/activitypub/objects/person.ts @@ -3,7 +3,7 @@ import { toUnicode } from 'punycode'; import * as debug from 'debug'; import config from '../../../config'; -import User, { validateUsername, isValidName, isValidDescription, IUser, IRemoteUser } from '../../../models/user'; +import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user'; import webFinger from '../../webfinger'; import Resolver from '../resolver'; import { resolveImage } from './image'; -- cgit v1.2.3-freya From d90ad34924a0641a08c67b1001d1567008d50ef3 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 9 Apr 2018 05:44:35 +0900 Subject: Fix bug --- src/remote/activitypub/objects/person.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/remote') diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts index a78538c4bd..f7ec064cdb 100644 --- a/src/remote/activitypub/objects/person.ts +++ b/src/remote/activitypub/objects/person.ts @@ -108,7 +108,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise img == null ? Promise.resolve(null) - : resolveImage(user, img.url) + : resolveImage(user, img) ))).map(file => file != null ? file._id : null); User.update({ _id: user._id }, { $set: { avatarId, bannerId } }); -- cgit v1.2.3-freya From 805bc4b25433cdc3eda068ec2c7be7260381dcef Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 10 Apr 2018 02:12:17 +0900 Subject: Refactor --- src/queue/processors/http/deliver.ts | 2 +- src/remote/activitypub/request.ts | 44 ++++++++++++++++++++++++++++++++++++ src/remote/request.ts | 44 ------------------------------------ 3 files changed, 45 insertions(+), 45 deletions(-) create mode 100644 src/remote/activitypub/request.ts delete mode 100644 src/remote/request.ts (limited to 'src/remote') diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts index 422e355b5f..cf843fad07 100644 --- a/src/queue/processors/http/deliver.ts +++ b/src/queue/processors/http/deliver.ts @@ -1,6 +1,6 @@ import * as kue from 'kue'; -import request from '../../../remote/request'; +import request from '../../../remote/activitypub/request'; export default async (job: kue.Job, done): Promise => { try { 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/request.ts b/src/remote/request.ts deleted file mode 100644 index 81e7c05c73..0000000000 --- a/src/remote/request.ts +++ /dev/null @@ -1,44 +0,0 @@ -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)); -}); -- cgit v1.2.3-freya From cb85d7cb14b818d5fa198fb98f800cc31e56ad2a Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 10 Apr 2018 04:38:02 +0900 Subject: 再帰的にNoteをレンダリングするように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/remote/activitypub/renderer/note.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src/remote') diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index 7cc388dc33..c364b13249 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -5,7 +5,7 @@ import DriveFile from '../../../models/drive-file'; import Note, { INote } from '../../../models/note'; import User from '../../../models/user'; -export default async (note: INote) => { +export default async function renderNote(note: INote, dive = true) { const promisedFiles = note.mediaIds ? DriveFile.find({ _id: { $in: note.mediaIds } }) : Promise.resolve([]); @@ -23,7 +23,15 @@ export default async (note: INote) => { }); if (inReplyToUser !== null) { - inReplyTo = inReplyToNote.uri || `${config.url}/notes/${inReplyToNote._id}`; + if (inReplyToNote.uri) { + inReplyTo = inReplyToNote.uri; + } else { + if (dive) { + inReplyTo = await renderNote(inReplyToNote, false); + } else { + inReplyTo = `${config.url}/notes/${inReplyToNote._id}`; + } + } } } } else { @@ -48,4 +56,4 @@ export default async (note: INote) => { attachment: (await promisedFiles).map(renderDocument), tag: (note.tags || []).map(renderHashtag) }; -}; +} -- cgit v1.2.3-freya