From d7c13b975f55c85b695b72a3ded3d5de97227414 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Wed, 4 Apr 2018 22:45:55 +0900 Subject: Retry HTTP requests --- src/queue/processors/db/delete-post-dependents.ts | 22 +++++++ src/queue/processors/db/index.ts | 7 +++ src/queue/processors/http/deliver-post.ts | 21 +++++++ src/queue/processors/http/follow.ts | 69 ++++++++++++++++++++++ src/queue/processors/http/index.ts | 17 ++++++ src/queue/processors/http/perform-activitypub.ts | 7 +++ src/queue/processors/http/process-inbox.ts | 39 ++++++++++++ src/queue/processors/http/report-github-failure.ts | 24 ++++++++ src/queue/processors/http/unfollow.ts | 63 ++++++++++++++++++++ 9 files changed, 269 insertions(+) create mode 100644 src/queue/processors/db/delete-post-dependents.ts create mode 100644 src/queue/processors/db/index.ts create mode 100644 src/queue/processors/http/deliver-post.ts create mode 100644 src/queue/processors/http/follow.ts create mode 100644 src/queue/processors/http/index.ts create mode 100644 src/queue/processors/http/perform-activitypub.ts create mode 100644 src/queue/processors/http/process-inbox.ts create mode 100644 src/queue/processors/http/report-github-failure.ts create mode 100644 src/queue/processors/http/unfollow.ts (limited to 'src/queue/processors') diff --git a/src/queue/processors/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts new file mode 100644 index 0000000000..6de21eb053 --- /dev/null +++ b/src/queue/processors/db/delete-post-dependents.ts @@ -0,0 +1,22 @@ +import Favorite from '../../../models/favorite'; +import Notification from '../../../models/notification'; +import PollVote from '../../../models/poll-vote'; +import PostReaction from '../../../models/post-reaction'; +import PostWatching from '../../../models/post-watching'; +import Post from '../../../models/post'; + +export default async ({ data }) => Promise.all([ + Favorite.remove({ postId: data._id }), + Notification.remove({ postId: data._id }), + PollVote.remove({ postId: data._id }), + PostReaction.remove({ postId: data._id }), + PostWatching.remove({ postId: data._id }), + Post.find({ repostId: data._id }).then(reposts => Promise.all([ + Notification.remove({ + postId: { + $in: reposts.map(({ _id }) => _id) + } + }), + Post.remove({ repostId: data._id }) + ])) +]); diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts new file mode 100644 index 0000000000..75838c099b --- /dev/null +++ b/src/queue/processors/db/index.ts @@ -0,0 +1,7 @@ +import deletePostDependents from './delete-post-dependents'; + +const handlers = { + deletePostDependents +}; + +export default (job, done) => handlers[job.data.type](job).then(() => done(), done); diff --git a/src/queue/processors/http/deliver-post.ts b/src/queue/processors/http/deliver-post.ts new file mode 100644 index 0000000000..e743fc5f68 --- /dev/null +++ b/src/queue/processors/http/deliver-post.ts @@ -0,0 +1,21 @@ +import Post from '../../../models/post'; +import User, { IRemoteUser } from '../../../models/user'; +import context from '../../../remote/activitypub/renderer/context'; +import renderCreate from '../../../remote/activitypub/renderer/create'; +import renderNote from '../../../remote/activitypub/renderer/note'; +import request from '../../../remote/request'; + +export default async ({ data }) => { + const promisedTo = User.findOne({ _id: data.toId }) as Promise; + const [from, post] = await Promise.all([ + User.findOne({ _id: data.fromId }), + Post.findOne({ _id: data.postId }) + ]); + const note = await renderNote(from, post); + const to = await promisedTo; + const create = renderCreate(note); + + create['@context'] = context; + + return request(from, to.account.inbox, create); +}; diff --git a/src/queue/processors/http/follow.ts b/src/queue/processors/http/follow.ts new file mode 100644 index 0000000000..4cb72828e7 --- /dev/null +++ b/src/queue/processors/http/follow.ts @@ -0,0 +1,69 @@ +import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user'; +import Following from '../../../models/following'; +import FollowingLog from '../../../models/following-log'; +import FollowedLog from '../../../models/followed-log'; +import event from '../../../publishers/stream'; +import notify from '../../../publishers/notify'; +import context from '../../../remote/activitypub/renderer/context'; +import render from '../../../remote/activitypub/renderer/follow'; +import request from '../../../remote/request'; +import Logger from '../../../utils/logger'; + +export default async ({ data }) => { + const { followerId, followeeId } = await Following.findOne({ _id: data.following }); + const [follower, followee] = await Promise.all([ + User.findOne({ _id: followerId }), + User.findOne({ _id: followeeId }) + ]); + + if (isLocalUser(follower) && isRemoteUser(followee)) { + const rendered = render(follower, followee); + rendered['@context'] = context; + + await request(follower, followee.account.inbox, rendered); + } + + try { + await Promise.all([ + // Increment following count + User.update(followerId, { + $inc: { + followingCount: 1 + } + }), + + FollowingLog.insert({ + createdAt: data.following.createdAt, + userId: followerId, + count: follower.followingCount + 1 + }), + + // Increment followers count + User.update({ _id: followeeId }, { + $inc: { + followersCount: 1 + } + }), + + FollowedLog.insert({ + createdAt: data.following.createdAt, + userId: followerId, + count: followee.followersCount + 1 + }), + + // Publish follow event + isLocalUser(follower) && packUser(followee, follower) + .then(packed => event(follower._id, 'follow', packed)), + + isLocalUser(followee) && Promise.all([ + packUser(follower, followee) + .then(packed => event(followee._id, 'followed', packed)), + + // Notify + isLocalUser(followee) && notify(followeeId, followerId, 'follow') + ]) + ]); + } catch (error) { + Logger.error(error.toString()); + } +}; diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts new file mode 100644 index 0000000000..8f9aa717c3 --- /dev/null +++ b/src/queue/processors/http/index.ts @@ -0,0 +1,17 @@ +import deliverPost from './deliver-post'; +import follow from './follow'; +import performActivityPub from './perform-activitypub'; +import processInbox from './process-inbox'; +import reportGitHubFailure from './report-github-failure'; +import unfollow from './unfollow'; + +const handlers = { + deliverPost, + follow, + performActivityPub, + processInbox, + reportGitHubFailure, + unfollow +}; + +export default (job, done) => handlers[job.data.type](job).then(() => done(), done); diff --git a/src/queue/processors/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts new file mode 100644 index 0000000000..7b84400d5c --- /dev/null +++ b/src/queue/processors/http/perform-activitypub.ts @@ -0,0 +1,7 @@ +import User from '../../../models/user'; +import act from '../../../remote/activitypub/act'; +import Resolver from '../../../remote/activitypub/resolver'; + +export default ({ data }) => User.findOne({ _id: data.actor }) + .then(actor => act(new Resolver(), actor, data.outbox)) + .then(Promise.all); diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts new file mode 100644 index 0000000000..de1dbd2f98 --- /dev/null +++ b/src/queue/processors/http/process-inbox.ts @@ -0,0 +1,39 @@ +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 Resolver from '../../../remote/activitypub/resolver'; + +export default async ({ data }): Promise => { + const keyIdLower = data.signature.keyId.toLowerCase(); + let user; + + if (keyIdLower.startsWith('acct:')) { + const { username, host } = parseAcct(keyIdLower.slice('acct:'.length)); + if (host === null) { + throw 'request was made by local user'; + } + + user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser; + } else { + user = await User.findOne({ + host: { $ne: null }, + 'account.publicKey.id': data.signature.keyId + }) as IRemoteUser; + + if (user === null) { + user = await resolvePerson(data.signature.keyId); + } + } + + if (user === null) { + throw 'failed to resolve user'; + } + + if (!verifySignature(data.signature, user.account.publicKey.publicKeyPem)) { + throw 'signature verification failed'; + } + + await Promise.all(await act(new Resolver(), user, data.inbox, true)); +}; diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts new file mode 100644 index 0000000000..21683ba3c2 --- /dev/null +++ b/src/queue/processors/http/report-github-failure.ts @@ -0,0 +1,24 @@ +import * as request from 'request-promise-native'; +import User from '../../../models/user'; +const createPost = require('../../../server/api/endpoints/posts/create'); + +export default async ({ data }) => { + const asyncBot = User.findOne({ _id: data.userId }); + + // Fetch parent status + const parentStatuses = await request({ + url: `${data.parentUrl}/statuses`, + headers: { + 'User-Agent': 'misskey' + }, + json: true + }); + + const parentState = parentStatuses[0].state; + const stillFailed = parentState == 'failure' || parentState == 'error'; + const text = stillFailed ? + `**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` : + `**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`; + + createPost({ text }, await asyncBot); +}; diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts new file mode 100644 index 0000000000..801a3612a7 --- /dev/null +++ b/src/queue/processors/http/unfollow.ts @@ -0,0 +1,63 @@ +import FollowedLog from '../../../models/followed-log'; +import Following from '../../../models/following'; +import FollowingLog from '../../../models/following-log'; +import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user'; +import stream from '../../../publishers/stream'; +import renderFollow from '../../../remote/activitypub/renderer/follow'; +import renderUndo from '../../../remote/activitypub/renderer/undo'; +import context from '../../../remote/activitypub/renderer/context'; +import request from '../../../remote/request'; +import Logger from '../../../utils/logger'; + +export default async ({ data }) => { + const following = await Following.findOne({ _id: data.id }); + if (following === null) { + return; + } + + const [follower, followee] = await Promise.all([ + User.findOne({ _id: following.followerId }), + User.findOne({ _id: following.followeeId }) + ]); + + if (isLocalUser(follower) && isRemoteUser(followee)) { + const undo = renderUndo(renderFollow(follower, followee)); + undo['@context'] = context; + + await request(follower, followee.account.inbox, undo); + } + + try { + await Promise.all([ + // Delete following + Following.findOneAndDelete({ _id: data.id }), + + // Decrement following count + User.update({ _id: follower._id }, { $inc: { followingCount: -1 } }), + FollowingLog.insert({ + createdAt: new Date(), + userId: follower._id, + count: follower.followingCount - 1 + }), + + // Decrement followers count + User.update({ _id: followee._id }, { $inc: { followersCount: -1 } }), + FollowedLog.insert({ + createdAt: new Date(), + userId: followee._id, + count: followee.followersCount - 1 + }) + ]); + + if (isLocalUser(follower)) { + return; + } + + const promisedPackedUser = packUser(followee, follower); + + // Publish follow event + stream(follower._id, 'unfollow', promisedPackedUser); + } catch (error) { + Logger.error(error.toString()); + } +}; -- cgit v1.2.3-freya From e8b42d7e1668679e6a6ee0a7aea1e2ff7f37005b Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 4 Apr 2018 23:12:35 +0900 Subject: wip --- src/post/create.ts | 107 +++++++++++++- src/processor/db/delete-post-dependents.ts | 22 --- src/processor/db/index.ts | 7 - src/processor/http/deliver-post.ts | 93 ------------ src/processor/http/follow.ts | 69 --------- src/processor/http/index.ts | 17 --- src/processor/http/perform-activitypub.ts | 7 - src/processor/http/process-inbox.ts | 39 ----- src/processor/http/report-github-failure.ts | 24 ---- src/processor/http/unfollow.ts | 56 -------- src/processor/index.ts | 18 --- src/queue.ts | 10 -- src/queue/index.ts | 37 +++++ src/queue/processors/db/delete-post-dependents.ts | 22 +++ src/queue/processors/db/index.ts | 7 + src/queue/processors/http/deliver.ts | 17 +++ src/queue/processors/http/follow.ts | 69 +++++++++ src/queue/processors/http/index.ts | 17 +++ src/queue/processors/http/perform-activitypub.ts | 7 + src/queue/processors/http/process-inbox.ts | 55 +++++++ src/queue/processors/http/report-github-failure.ts | 24 ++++ src/queue/processors/http/unfollow.ts | 56 ++++++++ src/queue/processors/index.ts | 18 +++ src/remote/activitypub/act/create.ts | 90 +++++++++++- src/remote/activitypub/act/index.ts | 54 ++++--- src/remote/activitypub/create.ts | 158 --------------------- src/remote/activitypub/resolver.ts | 75 +++++----- src/server/activitypub/inbox.ts | 2 +- 28 files changed, 577 insertions(+), 600 deletions(-) delete mode 100644 src/processor/db/delete-post-dependents.ts delete mode 100644 src/processor/db/index.ts delete mode 100644 src/processor/http/deliver-post.ts delete mode 100644 src/processor/http/follow.ts delete mode 100644 src/processor/http/index.ts delete mode 100644 src/processor/http/perform-activitypub.ts delete mode 100644 src/processor/http/process-inbox.ts delete mode 100644 src/processor/http/report-github-failure.ts delete mode 100644 src/processor/http/unfollow.ts delete mode 100644 src/processor/index.ts delete mode 100644 src/queue.ts create mode 100644 src/queue/index.ts create mode 100644 src/queue/processors/db/delete-post-dependents.ts create mode 100644 src/queue/processors/db/index.ts create mode 100644 src/queue/processors/http/deliver.ts create mode 100644 src/queue/processors/http/follow.ts create mode 100644 src/queue/processors/http/index.ts create mode 100644 src/queue/processors/http/perform-activitypub.ts create mode 100644 src/queue/processors/http/process-inbox.ts create mode 100644 src/queue/processors/http/report-github-failure.ts create mode 100644 src/queue/processors/http/unfollow.ts create mode 100644 src/queue/processors/index.ts delete mode 100644 src/remote/activitypub/create.ts (limited to 'src/queue/processors') diff --git a/src/post/create.ts b/src/post/create.ts index ecea37382d..f78bbe7521 100644 --- a/src/post/create.ts +++ b/src/post/create.ts @@ -1,8 +1,14 @@ import parseAcct from '../acct/parse'; -import Post from '../models/post'; -import User from '../models/user'; +import Post, { pack } from '../models/post'; +import User, { isLocalUser, isRemoteUser, IUser } from '../models/user'; +import stream from '../publishers/stream'; +import Following from '../models/following'; +import { createHttp } from '../queue'; +import renderNote from '../remote/activitypub/renderer/note'; +import renderCreate from '../remote/activitypub/renderer/create'; +import context from '../remote/activitypub/renderer/context'; -export default async (post, reply, repost, atMentions) => { +export default async (user: IUser, post, reply, repost, atMentions) => { post.mentions = []; function addMention(mentionee) { @@ -46,5 +52,98 @@ export default async (post, reply, repost, atMentions) => { addMention(_id); })); - return Post.insert(post); + const inserted = await Post.insert(post); + + User.update({ _id: user._id }, { + // Increment my posts count + $inc: { + postsCount: 1 + }, + + $set: { + latestPost: post._id + } + }); + + const postObj = await pack(inserted); + + // タイムラインへの投稿 + if (!post.channelId) { + // Publish event to myself's stream + stream(post.userId, 'post', postObj); + + // Fetch all followers + const followers = await Following.aggregate([{ + $lookup: { + from: 'users', + localField: 'followerId', + foreignField: '_id', + as: 'follower' + } + }, { + $match: { + followeeId: post.userId + } + }], { + _id: false + }); + + const note = await renderNote(user, post); + const content = renderCreate(note); + content['@context'] = context; + + Promise.all(followers.map(({ follower }) => { + if (isLocalUser(follower)) { + // Publish event to followers stream + stream(follower._id, 'post', postObj); + } else { + // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 + if (isLocalUser(user)) { + createHttp({ + type: 'deliver', + user, + content, + to: follower.account.inbox + }).save(); + } + } + })); + } + + // チャンネルへの投稿 + /* TODO + if (post.channelId) { + promises.push( + // Increment channel index(posts count) + Channel.update({ _id: post.channelId }, { + $inc: { + index: 1 + } + }), + + // Publish event to channel + promisedPostObj.then(postObj => { + publishChannelStream(post.channelId, 'post', postObj); + }), + + Promise.all([ + promisedPostObj, + + // Get channel watchers + ChannelWatching.find({ + channelId: post.channelId, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }) + ]).then(([postObj, watches]) => { + // チャンネルの視聴者(のタイムライン)に配信 + watches.forEach(w => { + stream(w.userId, 'post', postObj); + }); + }) + ); + }*/ + + return Promise.all(promises); + }; diff --git a/src/processor/db/delete-post-dependents.ts b/src/processor/db/delete-post-dependents.ts deleted file mode 100644 index 879c41ec9c..0000000000 --- a/src/processor/db/delete-post-dependents.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Favorite from '../../models/favorite'; -import Notification from '../../models/notification'; -import PollVote from '../../models/poll-vote'; -import PostReaction from '../../models/post-reaction'; -import PostWatching from '../../models/post-watching'; -import Post from '../../models/post'; - -export default async ({ data }) => Promise.all([ - Favorite.remove({ postId: data._id }), - Notification.remove({ postId: data._id }), - PollVote.remove({ postId: data._id }), - PostReaction.remove({ postId: data._id }), - PostWatching.remove({ postId: data._id }), - Post.find({ repostId: data._id }).then(reposts => Promise.all([ - Notification.remove({ - postId: { - $in: reposts.map(({ _id }) => _id) - } - }), - Post.remove({ repostId: data._id }) - ])) -]); diff --git a/src/processor/db/index.ts b/src/processor/db/index.ts deleted file mode 100644 index 75838c099b..0000000000 --- a/src/processor/db/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import deletePostDependents from './delete-post-dependents'; - -const handlers = { - deletePostDependents -}; - -export default (job, done) => handlers[job.data.type](job).then(() => done(), done); diff --git a/src/processor/http/deliver-post.ts b/src/processor/http/deliver-post.ts deleted file mode 100644 index c00ab912c9..0000000000 --- a/src/processor/http/deliver-post.ts +++ /dev/null @@ -1,93 +0,0 @@ -import Channel from '../../models/channel'; -import Following from '../../models/following'; -import ChannelWatching from '../../models/channel-watching'; -import Post, { pack } from '../../models/post'; -import User, { isLocalUser } from '../../models/user'; -import stream, { publishChannelStream } from '../../publishers/stream'; -import context from '../../remote/activitypub/renderer/context'; -import renderCreate from '../../remote/activitypub/renderer/create'; -import renderNote from '../../remote/activitypub/renderer/note'; -import request from '../../remote/request'; - -export default ({ data }) => Post.findOne({ _id: data.id }).then(post => { - const promisedPostObj = pack(post); - const promises = []; - - // タイムラインへの投稿 - if (!post.channelId) { - promises.push( - // Publish event to myself's stream - promisedPostObj.then(postObj => { - stream(post.userId, 'post', postObj); - }), - - Promise.all([ - User.findOne({ _id: post.userId }), - - // Fetch all followers - Following.aggregate([{ - $lookup: { - from: 'users', - localField: 'followerId', - foreignField: '_id', - as: 'follower' - } - }, { - $match: { - followeeId: post.userId - } - }], { - _id: false - }) - ]).then(([user, followers]) => Promise.all(followers.map(following => { - if (isLocalUser(following.follower)) { - // Publish event to followers stream - return promisedPostObj.then(postObj => { - stream(following.followerId, 'post', postObj); - }); - } - - return renderNote(user, post).then(note => { - const create = renderCreate(note); - create['@context'] = context; - return request(user, following.follower[0].account.inbox, create); - }); - }))) - ); - } - - // チャンネルへの投稿 - if (post.channelId) { - promises.push( - // Increment channel index(posts count) - Channel.update({ _id: post.channelId }, { - $inc: { - index: 1 - } - }), - - // Publish event to channel - promisedPostObj.then(postObj => { - publishChannelStream(post.channelId, 'post', postObj); - }), - - Promise.all([ - promisedPostObj, - - // Get channel watchers - ChannelWatching.find({ - channelId: post.channelId, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }) - ]).then(([postObj, watches]) => { - // チャンネルの視聴者(のタイムライン)に配信 - watches.forEach(w => { - stream(w.userId, 'post', postObj); - }); - }) - ); - } - - return Promise.all(promises); -}); diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts deleted file mode 100644 index 8bf890efbc..0000000000 --- a/src/processor/http/follow.ts +++ /dev/null @@ -1,69 +0,0 @@ -import User, { isLocalUser, pack as packUser } from '../../models/user'; -import Following from '../../models/following'; -import FollowingLog from '../../models/following-log'; -import FollowedLog from '../../models/followed-log'; -import event from '../../publishers/stream'; -import notify from '../../publishers/notify'; -import context from '../../remote/activitypub/renderer/context'; -import render from '../../remote/activitypub/renderer/follow'; -import request from '../../remote/request'; - -export default ({ data }) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => { - const promisedFollower = User.findOne({ _id: followerId }); - const promisedFollowee = User.findOne({ _id: followeeId }); - - return Promise.all([ - // Increment following count - User.update(followerId, { - $inc: { - followingCount: 1 - } - }), - - promisedFollower.then(({ followingCount }) => FollowingLog.insert({ - createdAt: data.following.createdAt, - userId: followerId, - count: followingCount + 1 - })), - - // Increment followers count - User.update({ _id: followeeId }, { - $inc: { - followersCount: 1 - } - }), - - promisedFollowee.then(({ followersCount }) => FollowedLog.insert({ - createdAt: data.following.createdAt, - userId: followerId, - count: followersCount + 1 - })), - - // Notify - promisedFollowee.then(followee => followee.host === null ? - notify(followeeId, followerId, 'follow') : null), - - // Publish follow event - Promise.all([promisedFollower, promisedFollowee]).then(([follower, followee]) => { - let followerEvent; - let followeeEvent; - - if (isLocalUser(follower)) { - followerEvent = packUser(followee, follower) - .then(packed => event(follower._id, 'follow', packed)); - } - - if (isLocalUser(followee)) { - followeeEvent = packUser(follower, followee) - .then(packed => event(followee._id, 'followed', packed)); - } else if (isLocalUser(follower)) { - const rendered = render(follower, followee); - rendered['@context'] = context; - - followeeEvent = request(follower, followee.account.inbox, rendered); - } - - return Promise.all([followerEvent, followeeEvent]); - }) - ]); -}); diff --git a/src/processor/http/index.ts b/src/processor/http/index.ts deleted file mode 100644 index 8f9aa717c3..0000000000 --- a/src/processor/http/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import deliverPost from './deliver-post'; -import follow from './follow'; -import performActivityPub from './perform-activitypub'; -import processInbox from './process-inbox'; -import reportGitHubFailure from './report-github-failure'; -import unfollow from './unfollow'; - -const handlers = { - deliverPost, - follow, - performActivityPub, - processInbox, - reportGitHubFailure, - unfollow -}; - -export default (job, done) => handlers[job.data.type](job).then(() => done(), done); diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts deleted file mode 100644 index 963e532fe5..0000000000 --- a/src/processor/http/perform-activitypub.ts +++ /dev/null @@ -1,7 +0,0 @@ -import User from '../../models/user'; -import act from '../../remote/activitypub/act'; -import Resolver from '../../remote/activitypub/resolver'; - -export default ({ data }) => User.findOne({ _id: data.actor }) - .then(actor => act(new Resolver(), actor, data.outbox)) - .then(Promise.all); diff --git a/src/processor/http/process-inbox.ts b/src/processor/http/process-inbox.ts deleted file mode 100644 index f102f8d6b4..0000000000 --- a/src/processor/http/process-inbox.ts +++ /dev/null @@ -1,39 +0,0 @@ -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 Resolver from '../../remote/activitypub/resolver'; - -export default async ({ data }): Promise => { - const keyIdLower = data.signature.keyId.toLowerCase(); - let user; - - if (keyIdLower.startsWith('acct:')) { - const { username, host } = parseAcct(keyIdLower.slice('acct:'.length)); - if (host === null) { - throw 'request was made by local user'; - } - - user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser; - } else { - user = await User.findOne({ - host: { $ne: null }, - 'account.publicKey.id': data.signature.keyId - }) as IRemoteUser; - - if (user === null) { - user = await resolvePerson(data.signature.keyId); - } - } - - if (user === null) { - throw 'failed to resolve user'; - } - - if (!verifySignature(data.signature, user.account.publicKey.publicKeyPem)) { - throw 'signature verification failed'; - } - - await Promise.all(await act(new Resolver(), user, data.inbox, true)); -}; diff --git a/src/processor/http/report-github-failure.ts b/src/processor/http/report-github-failure.ts deleted file mode 100644 index 4f6f5ccee5..0000000000 --- a/src/processor/http/report-github-failure.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as request from 'request-promise-native'; -import User from '../../models/user'; -const createPost = require('../../server/api/endpoints/posts/create'); - -export default async ({ data }) => { - const asyncBot = User.findOne({ _id: data.userId }); - - // Fetch parent status - const parentStatuses = await request({ - url: `${data.parentUrl}/statuses`, - headers: { - 'User-Agent': 'misskey' - }, - json: true - }); - - const parentState = parentStatuses[0].state; - const stillFailed = parentState == 'failure' || parentState == 'error'; - const text = stillFailed ? - `**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` : - `**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`; - - createPost({ text }, await asyncBot); -}; diff --git a/src/processor/http/unfollow.ts b/src/processor/http/unfollow.ts deleted file mode 100644 index d3d5f2246f..0000000000 --- a/src/processor/http/unfollow.ts +++ /dev/null @@ -1,56 +0,0 @@ -import FollowedLog from '../../models/followed-log'; -import Following from '../../models/following'; -import FollowingLog from '../../models/following-log'; -import User, { isRemoteUser, pack as packUser } from '../../models/user'; -import stream from '../../publishers/stream'; -import renderFollow from '../../remote/activitypub/renderer/follow'; -import renderUndo from '../../remote/activitypub/renderer/undo'; -import context from '../../remote/activitypub/renderer/context'; -import request from '../../remote/request'; - -export default async ({ data }) => { - // Delete following - const following = await Following.findOneAndDelete({ _id: data.id }); - if (following === null) { - return; - } - - const promisedFollower = User.findOne({ _id: following.followerId }); - const promisedFollowee = User.findOne({ _id: following.followeeId }); - - await Promise.all([ - // Decrement following count - User.update({ _id: following.followerId }, { $inc: { followingCount: -1 } }), - promisedFollower.then(({ followingCount }) => FollowingLog.insert({ - createdAt: new Date(), - userId: following.followerId, - count: followingCount - 1 - })), - - // Decrement followers count - User.update({ _id: following.followeeId }, { $inc: { followersCount: -1 } }), - promisedFollowee.then(({ followersCount }) => FollowedLog.insert({ - createdAt: new Date(), - userId: following.followeeId, - count: followersCount - 1 - })), - - // Publish follow event - Promise.all([promisedFollower, promisedFollowee]).then(async ([follower, followee]) => { - if (isRemoteUser(follower)) { - return; - } - - const promisedPackedUser = packUser(followee, follower); - - if (isRemoteUser(followee)) { - const undo = renderUndo(renderFollow(follower, followee)); - undo['@context'] = context; - - await request(follower, followee.account.inbox, undo); - } - - stream(follower._id, 'unfollow', promisedPackedUser); - }) - ]); -}; diff --git a/src/processor/index.ts b/src/processor/index.ts deleted file mode 100644 index 172048ddae..0000000000 --- a/src/processor/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import queue from '../queue'; -import db from './db'; -import http from './http'; - -export default () => { - queue.process('db', db); - - /* - 256 is the default concurrency limit of Mozilla Firefox and Google - Chromium. - - a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google - https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff - Network.http.max-connections - MozillaZine Knowledge Base - http://kb.mozillazine.org/Network.http.max-connections - */ - queue.process('http', 256, http); -}; diff --git a/src/queue.ts b/src/queue.ts deleted file mode 100644 index 08ea13c2a3..0000000000 --- a/src/queue.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createQueue } from 'kue'; -import config from './config'; - -export default createQueue({ - redis: { - port: config.redis.port, - host: config.redis.host, - auth: config.redis.pass - } -}); diff --git a/src/queue/index.ts b/src/queue/index.ts new file mode 100644 index 0000000000..c8c436b18c --- /dev/null +++ b/src/queue/index.ts @@ -0,0 +1,37 @@ +import { createQueue } from 'kue'; +import config from '../config'; +import db from './processors/db'; +import http from './processors/http'; + +const queue = createQueue({ + redis: { + port: config.redis.port, + host: config.redis.host, + auth: config.redis.pass + } +}); + +export function createHttp(data) { + return queue + .create('http', data) + .attempts(16) + .backoff({ delay: 16384, type: 'exponential' }); +} + +export function createDb(data) { + return queue.create('db', data); +} + +export function process() { + queue.process('db', db); + + /* + 256 is the default concurrency limit of Mozilla Firefox and Google + Chromium. + a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google + https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff + Network.http.max-connections - MozillaZine Knowledge Base + http://kb.mozillazine.org/Network.http.max-connections + */ + queue.process('http', 256, http); +} diff --git a/src/queue/processors/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts new file mode 100644 index 0000000000..879c41ec9c --- /dev/null +++ b/src/queue/processors/db/delete-post-dependents.ts @@ -0,0 +1,22 @@ +import Favorite from '../../models/favorite'; +import Notification from '../../models/notification'; +import PollVote from '../../models/poll-vote'; +import PostReaction from '../../models/post-reaction'; +import PostWatching from '../../models/post-watching'; +import Post from '../../models/post'; + +export default async ({ data }) => Promise.all([ + Favorite.remove({ postId: data._id }), + Notification.remove({ postId: data._id }), + PollVote.remove({ postId: data._id }), + PostReaction.remove({ postId: data._id }), + PostWatching.remove({ postId: data._id }), + Post.find({ repostId: data._id }).then(reposts => Promise.all([ + Notification.remove({ + postId: { + $in: reposts.map(({ _id }) => _id) + } + }), + Post.remove({ repostId: data._id }) + ])) +]); diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts new file mode 100644 index 0000000000..75838c099b --- /dev/null +++ b/src/queue/processors/db/index.ts @@ -0,0 +1,7 @@ +import deletePostDependents from './delete-post-dependents'; + +const handlers = { + deletePostDependents +}; + +export default (job, done) => handlers[job.data.type](job).then(() => done(), done); diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts new file mode 100644 index 0000000000..8cd9eb624e --- /dev/null +++ b/src/queue/processors/http/deliver.ts @@ -0,0 +1,17 @@ +import * as kue from 'kue'; + +import Channel from '../../models/channel'; +import Following from '../../models/following'; +import ChannelWatching from '../../models/channel-watching'; +import Post, { pack } from '../../models/post'; +import User, { isLocalUser } from '../../models/user'; +import stream, { publishChannelStream } from '../../publishers/stream'; +import context from '../../remote/activitypub/renderer/context'; +import renderCreate from '../../remote/activitypub/renderer/create'; +import renderNote from '../../remote/activitypub/renderer/note'; +import request from '../../remote/request'; + +export default async (job: kue.Job, done): Promise => { + + request(user, following.follower[0].account.inbox, create); +} diff --git a/src/queue/processors/http/follow.ts b/src/queue/processors/http/follow.ts new file mode 100644 index 0000000000..8bf890efbc --- /dev/null +++ b/src/queue/processors/http/follow.ts @@ -0,0 +1,69 @@ +import User, { isLocalUser, pack as packUser } from '../../models/user'; +import Following from '../../models/following'; +import FollowingLog from '../../models/following-log'; +import FollowedLog from '../../models/followed-log'; +import event from '../../publishers/stream'; +import notify from '../../publishers/notify'; +import context from '../../remote/activitypub/renderer/context'; +import render from '../../remote/activitypub/renderer/follow'; +import request from '../../remote/request'; + +export default ({ data }) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => { + const promisedFollower = User.findOne({ _id: followerId }); + const promisedFollowee = User.findOne({ _id: followeeId }); + + return Promise.all([ + // Increment following count + User.update(followerId, { + $inc: { + followingCount: 1 + } + }), + + promisedFollower.then(({ followingCount }) => FollowingLog.insert({ + createdAt: data.following.createdAt, + userId: followerId, + count: followingCount + 1 + })), + + // Increment followers count + User.update({ _id: followeeId }, { + $inc: { + followersCount: 1 + } + }), + + promisedFollowee.then(({ followersCount }) => FollowedLog.insert({ + createdAt: data.following.createdAt, + userId: followerId, + count: followersCount + 1 + })), + + // Notify + promisedFollowee.then(followee => followee.host === null ? + notify(followeeId, followerId, 'follow') : null), + + // Publish follow event + Promise.all([promisedFollower, promisedFollowee]).then(([follower, followee]) => { + let followerEvent; + let followeeEvent; + + if (isLocalUser(follower)) { + followerEvent = packUser(followee, follower) + .then(packed => event(follower._id, 'follow', packed)); + } + + if (isLocalUser(followee)) { + followeeEvent = packUser(follower, followee) + .then(packed => event(followee._id, 'followed', packed)); + } else if (isLocalUser(follower)) { + const rendered = render(follower, followee); + rendered['@context'] = context; + + followeeEvent = request(follower, followee.account.inbox, rendered); + } + + return Promise.all([followerEvent, followeeEvent]); + }) + ]); +}); diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts new file mode 100644 index 0000000000..8f9aa717c3 --- /dev/null +++ b/src/queue/processors/http/index.ts @@ -0,0 +1,17 @@ +import deliverPost from './deliver-post'; +import follow from './follow'; +import performActivityPub from './perform-activitypub'; +import processInbox from './process-inbox'; +import reportGitHubFailure from './report-github-failure'; +import unfollow from './unfollow'; + +const handlers = { + deliverPost, + follow, + performActivityPub, + processInbox, + reportGitHubFailure, + unfollow +}; + +export default (job, done) => handlers[job.data.type](job).then(() => done(), done); diff --git a/src/queue/processors/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts new file mode 100644 index 0000000000..963e532fe5 --- /dev/null +++ b/src/queue/processors/http/perform-activitypub.ts @@ -0,0 +1,7 @@ +import User from '../../models/user'; +import act from '../../remote/activitypub/act'; +import Resolver from '../../remote/activitypub/resolver'; + +export default ({ data }) => User.findOne({ _id: data.actor }) + .then(actor => act(new Resolver(), actor, data.outbox)) + .then(Promise.all); diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts new file mode 100644 index 0000000000..fff1fbf663 --- /dev/null +++ b/src/queue/processors/http/process-inbox.ts @@ -0,0 +1,55 @@ +import * as kue from 'kue'; + +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 Resolver from '../../remote/activitypub/resolver'; + +// ユーザーのinboxにアクティビティが届いた時の処理 +export default async (job: kue.Job, done): Promise => { + const signature = job.data.signature; + const activity = job.data.activity; + + const keyIdLower = signature.keyId.toLowerCase(); + let user; + + if (keyIdLower.startsWith('acct:')) { + const { username, host } = parseAcct(keyIdLower.slice('acct:'.length)); + if (host === null) { + console.warn(`request was made by local user: @${username}`); + done(); + } + + user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser; + } else { + user = await User.findOne({ + host: { $ne: null }, + 'account.publicKey.id': signature.keyId + }) as IRemoteUser; + + // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する + if (user === null) { + user = await resolvePerson(signature.keyId); + } + } + + if (user === null) { + done(new Error('failed to resolve user')); + return; + } + + if (!verifySignature(signature, user.account.publicKey.publicKeyPem)) { + done(new Error('signature verification failed')); + return; + } + + // アクティビティを処理 + try { + await act(new Resolver(), user, activity); + done(); + } catch (e) { + done(e); + } +}; diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts new file mode 100644 index 0000000000..4f6f5ccee5 --- /dev/null +++ b/src/queue/processors/http/report-github-failure.ts @@ -0,0 +1,24 @@ +import * as request from 'request-promise-native'; +import User from '../../models/user'; +const createPost = require('../../server/api/endpoints/posts/create'); + +export default async ({ data }) => { + const asyncBot = User.findOne({ _id: data.userId }); + + // Fetch parent status + const parentStatuses = await request({ + url: `${data.parentUrl}/statuses`, + headers: { + 'User-Agent': 'misskey' + }, + json: true + }); + + const parentState = parentStatuses[0].state; + const stillFailed = parentState == 'failure' || parentState == 'error'; + const text = stillFailed ? + `**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` : + `**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`; + + createPost({ text }, await asyncBot); +}; diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts new file mode 100644 index 0000000000..d3d5f2246f --- /dev/null +++ b/src/queue/processors/http/unfollow.ts @@ -0,0 +1,56 @@ +import FollowedLog from '../../models/followed-log'; +import Following from '../../models/following'; +import FollowingLog from '../../models/following-log'; +import User, { isRemoteUser, pack as packUser } from '../../models/user'; +import stream from '../../publishers/stream'; +import renderFollow from '../../remote/activitypub/renderer/follow'; +import renderUndo from '../../remote/activitypub/renderer/undo'; +import context from '../../remote/activitypub/renderer/context'; +import request from '../../remote/request'; + +export default async ({ data }) => { + // Delete following + const following = await Following.findOneAndDelete({ _id: data.id }); + if (following === null) { + return; + } + + const promisedFollower = User.findOne({ _id: following.followerId }); + const promisedFollowee = User.findOne({ _id: following.followeeId }); + + await Promise.all([ + // Decrement following count + User.update({ _id: following.followerId }, { $inc: { followingCount: -1 } }), + promisedFollower.then(({ followingCount }) => FollowingLog.insert({ + createdAt: new Date(), + userId: following.followerId, + count: followingCount - 1 + })), + + // Decrement followers count + User.update({ _id: following.followeeId }, { $inc: { followersCount: -1 } }), + promisedFollowee.then(({ followersCount }) => FollowedLog.insert({ + createdAt: new Date(), + userId: following.followeeId, + count: followersCount - 1 + })), + + // Publish follow event + Promise.all([promisedFollower, promisedFollowee]).then(async ([follower, followee]) => { + if (isRemoteUser(follower)) { + return; + } + + const promisedPackedUser = packUser(followee, follower); + + if (isRemoteUser(followee)) { + const undo = renderUndo(renderFollow(follower, followee)); + undo['@context'] = context; + + await request(follower, followee.account.inbox, undo); + } + + stream(follower._id, 'unfollow', promisedPackedUser); + }) + ]); +}; diff --git a/src/queue/processors/index.ts b/src/queue/processors/index.ts new file mode 100644 index 0000000000..172048ddae --- /dev/null +++ b/src/queue/processors/index.ts @@ -0,0 +1,18 @@ +import queue from '../queue'; +import db from './db'; +import http from './http'; + +export default () => { + queue.process('db', db); + + /* + 256 is the default concurrency limit of Mozilla Firefox and Google + Chromium. + + a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google + https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff + Network.http.max-connections - MozillaZine Knowledge Base + http://kb.mozillazine.org/Network.http.max-connections + */ + queue.process('http', 256, http); +}; diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts index fa681982cf..c1a30ce7d0 100644 --- a/src/remote/activitypub/act/create.ts +++ b/src/remote/activitypub/act/create.ts @@ -1,10 +1,92 @@ -import create from '../create'; +import { JSDOM } from 'jsdom'; +const createDOMPurify = require('dompurify'); + import Resolver from '../resolver'; +import DriveFile from '../../../models/drive-file'; +import Post from '../../../models/post'; +import uploadFromUrl from '../../../drive/upload-from-url'; +import createPost from '../../../post/create'; -export default (resolver: Resolver, actor, activity, distribute) => { +export default async (resolver: Resolver, actor, activity): Promise => { if ('actor' in activity && actor.account.uri !== activity.actor) { - throw new Error(); + throw new Error('invalid actor'); } - return create(resolver, actor, activity.object, distribute); + const uri = activity.id || activity; + + try { + await Promise.all([ + DriveFile.findOne({ 'metadata.uri': uri }).then(file => { + if (file !== null) { + throw new Error(); + } + }, () => {}), + Post.findOne({ uri }).then(post => { + if (post !== null) { + throw new Error(); + } + }, () => {}) + ]); + } catch (object) { + throw new Error(`already registered: ${uri}`); + } + + const object = await resolver.resolve(activity); + + switch (object.type) { + case 'Image': + createImage(resolver, object); + break; + + case 'Note': + createNote(resolver, object); + break; + } + + /// + + async function createImage(resolver: Resolver, image) { + if ('attributedTo' in image && actor.account.uri !== image.attributedTo) { + throw new Error('invalid image'); + } + + return await uploadFromUrl(image.url, actor); + } + + async function createNote(resolver: Resolver, note) { + if ( + ('attributedTo' in note && actor.account.uri !== note.attributedTo) || + typeof note.id !== 'string' + ) { + throw new Error('invalid note'); + } + + const mediaIds = []; + + if ('attachment' in note) { + note.attachment.forEach(async media => { + const created = await createImage(resolver, media); + mediaIds.push(created._id); + }); + } + + const { window } = new JSDOM(note.content); + + await createPost(actor, { + channelId: undefined, + index: undefined, + createdAt: new Date(note.published), + mediaIds, + replyId: undefined, + repostId: undefined, + poll: undefined, + text: window.document.body.textContent, + textHtml: note.content && createDOMPurify(window).sanitize(note.content), + userId: actor._id, + appId: null, + viaMobile: false, + geo: undefined, + uri: note.id + }, null, null, []); + } }; diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts index d282e12885..d78335f16e 100644 --- a/src/remote/activitypub/act/index.ts +++ b/src/remote/activitypub/act/index.ts @@ -2,35 +2,29 @@ import create from './create'; import performDeleteActivity from './delete'; import follow from './follow'; import undo from './undo'; -import createObject from '../create'; import Resolver from '../resolver'; - -export default async (parentResolver: Resolver, actor, value, distribute?: boolean) => { - const collection = await parentResolver.resolveCollection(value); - - return collection.object.map(async element => { - const { resolver, object } = await collection.resolver.resolveOne(element); - const created = await (await createObject(resolver, actor, [object], distribute))[0]; - - if (created !== null) { - return created; - } - - switch (object.type) { - case 'Create': - return create(resolver, actor, object, distribute); - - case 'Delete': - return performDeleteActivity(resolver, actor, object); - - case 'Follow': - return follow(resolver, actor, object, distribute); - - case 'Undo': - return undo(resolver, actor, object); - - default: - return null; - } - }); +import { IObject } from '../type'; + +export default async (parentResolver: Resolver, actor, activity: IObject): Promise => { + switch (activity.type) { + case 'Create': + await create(parentResolver, actor, activity); + break; + + case 'Delete': + await performDeleteActivity(parentResolver, actor, activity); + break; + + case 'Follow': + await follow(parentResolver, actor, activity); + break; + + case 'Undo': + await undo(parentResolver, actor, activity); + break; + + default: + console.warn(`unknown activity type: ${activity.type}`); + return null; + } }; diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts deleted file mode 100644 index 97c72860fd..0000000000 --- a/src/remote/activitypub/create.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { JSDOM } from 'jsdom'; -import { ObjectID } from 'mongodb'; -import config from '../../config'; -import DriveFile from '../../models/drive-file'; -import Post from '../../models/post'; -import { IRemoteUser } from '../../models/user'; -import uploadFromUrl from '../../drive/upload-from-url'; -import createPost from '../../post/create'; -import distributePost from '../../post/distribute'; -import Resolver from './resolver'; -const createDOMPurify = require('dompurify'); - -type IResult = { - resolver: Resolver; - object: { - $ref: string; - $id: ObjectID; - }; -}; - -class Creator { - private actor: IRemoteUser; - private distribute: boolean; - - constructor(actor, distribute) { - this.actor = actor; - this.distribute = distribute; - } - - private async createImage(resolver: Resolver, image) { - if ('attributedTo' in image && this.actor.account.uri !== image.attributedTo) { - throw new Error(); - } - - const { _id } = await uploadFromUrl(image.url, this.actor, image.id || null); - return { - resolver, - object: { $ref: 'driveFiles.files', $id: _id } - }; - } - - private async createNote(resolver: Resolver, note) { - if ( - ('attributedTo' in note && this.actor.account.uri !== note.attributedTo) || - typeof note.id !== 'string' - ) { - throw new Error(); - } - - const mediaIds = 'attachment' in note && - (await Promise.all(await this.create(resolver, note.attachment))) - .filter(media => media !== null && media.object.$ref === 'driveFiles.files') - .map(({ object }) => object.$id); - - const { window } = new JSDOM(note.content); - - const inserted = await createPost({ - channelId: undefined, - index: undefined, - createdAt: new Date(note.published), - mediaIds, - replyId: undefined, - repostId: undefined, - poll: undefined, - text: window.document.body.textContent, - textHtml: note.content && createDOMPurify(window).sanitize(note.content), - userId: this.actor._id, - appId: null, - viaMobile: false, - geo: undefined, - uri: note.id - }, null, null, []); - - const promises = []; - - if (this.distribute) { - promises.push(distributePost(this.actor, inserted.mentions, inserted)); - } - - // Register to search database - if (note.content && config.elasticsearch.enable) { - const es = require('../../db/elasticsearch'); - - promises.push(new Promise((resolve, reject) => { - es.index({ - index: 'misskey', - type: 'post', - id: inserted._id.toString(), - body: { - text: window.document.body.textContent - } - }, resolve); - })); - } - - await Promise.all(promises); - - return { - resolver, - object: { $ref: 'posts', id: inserted._id } - }; - } - - public async create(parentResolver: Resolver, value): Promise>> { - const collection = await parentResolver.resolveCollection(value); - - return collection.object.map(async element => { - const uri = element.id || element; - - try { - await Promise.all([ - DriveFile.findOne({ 'metadata.uri': uri }).then(file => { - if (file === null) { - return; - } - - throw { - $ref: 'driveFile.files', - $id: file._id - }; - }, () => {}), - Post.findOne({ uri }).then(post => { - if (post === null) { - return; - } - - throw { - $ref: 'posts', - $id: post._id - }; - }, () => {}) - ]); - } catch (object) { - return { - resolver: collection.resolver, - object - }; - } - - const { resolver, object } = await collection.resolver.resolveOne(element); - - switch (object.type) { - case 'Image': - return this.createImage(resolver, object); - - case 'Note': - return this.createNote(resolver, object); - } - - return null; - }); - } -} - -export default (resolver: Resolver, actor, value, distribute?: boolean) => { - const creator = new Creator(actor, distribute); - return creator.create(resolver, value); -}; diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 371ccdcc30..de0bba2687 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -1,20 +1,45 @@ +import { IObject } from "./type"; + const request = require('request-promise-native'); export default class Resolver { - private requesting: Set; + private history: Set; - constructor(iterable?: Iterable) { - this.requesting = new Set(iterable); + constructor() { + this.history = new Set(); } - private async resolveUnrequestedOne(value) { + public async resolveCollection(value) { + const collection = typeof value === 'string' + ? await this.resolve(value) + : value; + + switch (collection.type) { + case 'Collection': + collection.objects = collection.object.items; + break; + + case 'OrderedCollection': + collection.objects = collection.object.orderedItems; + break; + + default: + throw new Error(`unknown collection type: ${collection.type}`); + } + + return collection; + } + + public async resolve(value): Promise { if (typeof value !== 'string') { - return { resolver: this, object: value }; + return value; } - const resolver = new Resolver(this.requesting); + if (this.history.has(value)) { + throw new Error('cannot resolve already resolved one'); + } - resolver.requesting.add(value); + this.history.add(value); const object = await request({ url: value, @@ -29,41 +54,9 @@ export default class Resolver { !object['@context'].includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' )) { - throw new Error(); - } - - return { resolver, object }; - } - - public async resolveCollection(value) { - const resolved = typeof value === 'string' ? - await this.resolveUnrequestedOne(value) : - { resolver: this, object: value }; - - switch (resolved.object.type) { - case 'Collection': - resolved.object = resolved.object.items; - break; - - case 'OrderedCollection': - resolved.object = resolved.object.orderedItems; - break; - - default: - if (!Array.isArray(value)) { - resolved.object = [resolved.object]; - } - break; - } - - return resolved; - } - - public resolveOne(value) { - if (this.requesting.has(value)) { - throw new Error(); + throw new Error('invalid response'); } - return this.resolveUnrequestedOne(value); + return object; } } diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts index 5de8433850..847dc19af6 100644 --- a/src/server/activitypub/inbox.ts +++ b/src/server/activitypub/inbox.ts @@ -24,7 +24,7 @@ app.post('/@:user/inbox', bodyParser.json({ queue.create('http', { type: 'processInbox', - inbox: req.body, + activity: req.body, signature, }).save(); -- cgit v1.2.3-freya From 5bd1451b610c134c056991e05327990180cbb8d5 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 4 Apr 2018 23:22:48 +0900 Subject: wip --- src/queue/processors/http/deliver.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts index 8cd9eb624e..1700063a5d 100644 --- a/src/queue/processors/http/deliver.ts +++ b/src/queue/processors/http/deliver.ts @@ -1,17 +1,7 @@ import * as kue from 'kue'; -import Channel from '../../models/channel'; -import Following from '../../models/following'; -import ChannelWatching from '../../models/channel-watching'; -import Post, { pack } from '../../models/post'; -import User, { isLocalUser } from '../../models/user'; -import stream, { publishChannelStream } from '../../publishers/stream'; -import context from '../../remote/activitypub/renderer/context'; -import renderCreate from '../../remote/activitypub/renderer/create'; -import renderNote from '../../remote/activitypub/renderer/note'; -import request from '../../remote/request'; +import request from '../../../remote/request'; export default async (job: kue.Job, done): Promise => { - - request(user, following.follower[0].account.inbox, create); -} + await request(job.data.user, job.data.to, job.data.content); +}; -- cgit v1.2.3-freya From 77f056b4fcdf74da8b6a8cc4a923eb8789d6f5ae Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 4 Apr 2018 23:59:38 +0900 Subject: wip --- src/api/drive/add-file.ts | 314 ++++++++++++++++++++++++++++++++++ src/api/drive/upload-from-url.ts | 46 +++++ src/api/following/create.ts | 82 +++++++++ src/api/post/create.ts | 149 ++++++++++++++++ src/api/post/distribute.ts | 190 ++++++++++++++++++++ src/api/post/watch.ts | 26 +++ src/drive/add-file.ts | 314 ---------------------------------- src/drive/upload-from-url.ts | 46 ----- src/post/create.ts | 149 ---------------- src/post/distribute.ts | 190 -------------------- src/post/watch.ts | 26 --- src/queue/processors/http/unfollow.ts | 97 ++++++----- src/remote/activitypub/act/create.ts | 8 +- src/remote/activitypub/act/follow.ts | 59 +------ 14 files changed, 867 insertions(+), 829 deletions(-) create mode 100644 src/api/drive/add-file.ts create mode 100644 src/api/drive/upload-from-url.ts create mode 100644 src/api/following/create.ts create mode 100644 src/api/post/create.ts create mode 100644 src/api/post/distribute.ts create mode 100644 src/api/post/watch.ts delete mode 100644 src/drive/add-file.ts delete mode 100644 src/drive/upload-from-url.ts delete mode 100644 src/post/create.ts delete mode 100644 src/post/distribute.ts delete mode 100644 src/post/watch.ts (limited to 'src/queue/processors') diff --git a/src/api/drive/add-file.ts b/src/api/drive/add-file.ts new file mode 100644 index 0000000000..24eb5208d5 --- /dev/null +++ b/src/api/drive/add-file.ts @@ -0,0 +1,314 @@ +import { Buffer } from 'buffer'; +import * as fs from 'fs'; +import * as tmp from 'tmp'; +import * as stream from 'stream'; + +import * as mongodb from 'mongodb'; +import * as crypto from 'crypto'; +import * as _gm from 'gm'; +import * as debug from 'debug'; +import fileType = require('file-type'); +import prominence = require('prominence'); + +import DriveFile, { IMetadata, getGridFSBucket } from '../models/drive-file'; +import DriveFolder from '../models/drive-folder'; +import { pack } from '../models/drive-file'; +import event, { publishDriveStream } from '../publishers/stream'; +import getAcct from '../acct/render'; +import config from '../config'; + +const gm = _gm.subClass({ + imageMagick: true +}); + +const log = debug('misskey:drive:add-file'); + +const tmpFile = (): Promise => new Promise((resolve, reject) => { + tmp.file((e, path) => { + if (e) return reject(e); + resolve(path); + }); +}); + +const addToGridFS = (name: string, readable: stream.Readable, type: string, metadata: any): Promise => + getGridFSBucket() + .then(bucket => new Promise((resolve, reject) => { + const writeStream = bucket.openUploadStream(name, { contentType: type, metadata }); + writeStream.once('finish', (doc) => { resolve(doc); }); + writeStream.on('error', reject); + readable.pipe(writeStream); + })); + +const addFile = async ( + user: any, + path: string, + name: string = null, + comment: string = null, + folderId: mongodb.ObjectID = null, + force: boolean = false, + uri: string = null +) => { + log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`); + + // Calculate hash, get content type and get file size + const [hash, [mime, ext], size] = await Promise.all([ + // hash + ((): Promise => new Promise((res, rej) => { + const readable = fs.createReadStream(path); + const hash = crypto.createHash('md5'); + const chunks = []; + readable + .on('error', rej) + .pipe(hash) + .on('error', rej) + .on('data', (chunk) => chunks.push(chunk)) + .on('end', () => { + const buffer = Buffer.concat(chunks); + res(buffer.toString('hex')); + }); + }))(), + // mime + ((): Promise<[string, string | null]> => new Promise((res, rej) => { + const readable = fs.createReadStream(path); + readable + .on('error', rej) + .once('data', (buffer: Buffer) => { + readable.destroy(); + const type = fileType(buffer); + if (type) { + return res([type.mime, type.ext]); + } else { + // 種類が同定できなかったら application/octet-stream にする + return res(['application/octet-stream', null]); + } + }); + }))(), + // size + ((): Promise => new Promise((res, rej) => { + fs.stat(path, (err, stats) => { + if (err) return rej(err); + res(stats.size); + }); + }))() + ]); + + log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`); + + // detect name + const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled'); + + if (!force) { + // Check if there is a file with the same hash + const much = await DriveFile.findOne({ + md5: hash, + 'metadata.userId': user._id + }); + + if (much !== null) { + log('file with same hash is found'); + return much; + } else { + log('file with same hash is not found'); + } + } + + const [wh, averageColor, folder] = await Promise.all([ + // Width and height (when image) + (async () => { + // 画像かどうか + if (!/^image\/.*$/.test(mime)) { + return null; + } + + const imageType = mime.split('/')[1]; + + // 画像でもPNGかJPEGかGIFでないならスキップ + if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') { + return null; + } + + log('calculate image width and height...'); + + // Calculate width and height + const g = gm(fs.createReadStream(path), name); + const size = await prominence(g).size(); + + log(`image width and height is calculated: ${size.width}, ${size.height}`); + + return [size.width, size.height]; + })(), + // average color (when image) + (async () => { + // 画像かどうか + if (!/^image\/.*$/.test(mime)) { + return null; + } + + const imageType = mime.split('/')[1]; + + // 画像でもPNGかJPEGでないならスキップ + if (imageType != 'png' && imageType != 'jpeg') { + return null; + } + + log('calculate average color...'); + + const buffer = await prominence(gm(fs.createReadStream(path), name) + .setFormat('ppm') + .resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック + .toBuffer(); + + const r = buffer.readUInt8(buffer.length - 3); + const g = buffer.readUInt8(buffer.length - 2); + const b = buffer.readUInt8(buffer.length - 1); + + log(`average color is calculated: ${r}, ${g}, ${b}`); + + return [r, g, b]; + })(), + // folder + (async () => { + if (!folderId) { + return null; + } + const driveFolder = await DriveFolder.findOne({ + _id: folderId, + userId: user._id + }); + if (!driveFolder) { + throw 'folder-not-found'; + } + return driveFolder; + })(), + // usage checker + (async () => { + // Calculate drive usage + const usage = await DriveFile + .aggregate([{ + $match: { 'metadata.userId': user._id } + }, { + $project: { + length: true + } + }, { + $group: { + _id: null, + usage: { $sum: '$length' } + } + }]) + .then((aggregates: any[]) => { + if (aggregates.length > 0) { + return aggregates[0].usage; + } + return 0; + }); + + log(`drive usage is ${usage}`); + + // If usage limit exceeded + if (usage + size > user.driveCapacity) { + throw 'no-free-space'; + } + })() + ]); + + const readable = fs.createReadStream(path); + + const properties = {}; + + if (wh) { + properties['width'] = wh[0]; + properties['height'] = wh[1]; + } + + if (averageColor) { + properties['avgColor'] = averageColor; + } + + const metadata = { + userId: user._id, + folderId: folder !== null ? folder._id : null, + comment: comment, + properties: properties + } as IMetadata; + + if (uri !== null) { + metadata.uri = uri; + } + + return addToGridFS(detectedName, readable, mime, metadata); +}; + +/** + * Add file to drive + * + * @param user User who wish to add file + * @param file File path or readableStream + * @param comment Comment + * @param type File type + * @param folderId Folder ID + * @param force If set to true, forcibly upload the file even if there is a file with the same hash. + * @return Object that represents added file + */ +export default (user: any, file: string | stream.Readable, ...args) => new Promise((resolve, reject) => { + // Get file path + new Promise((res: (v: [string, boolean]) => void, rej) => { + if (typeof file === 'string') { + res([file, false]); + return; + } + if (typeof file === 'object' && typeof file.read === 'function') { + tmpFile() + .then(path => { + const readable: stream.Readable = file; + const writable = fs.createWriteStream(path); + readable + .on('error', rej) + .on('end', () => { + res([path, true]); + }) + .pipe(writable) + .on('error', rej); + }) + .catch(rej); + } + rej(new Error('un-compatible file.')); + }) + .then(([path, shouldCleanup]): Promise => new Promise((res, rej) => { + addFile(user, path, ...args) + .then(file => { + res(file); + if (shouldCleanup) { + fs.unlink(path, (e) => { + if (e) log(e.stack); + }); + } + }) + .catch(rej); + })) + .then(file => { + log(`drive file has been created ${file._id}`); + resolve(file); + + pack(file).then(serializedFile => { + // Publish drive_file_created event + event(user._id, 'drive_file_created', serializedFile); + publishDriveStream(user._id, 'file_created', serializedFile); + + // Register to search database + if (config.elasticsearch.enable) { + const es = require('../db/elasticsearch'); + es.index({ + index: 'misskey', + type: 'drive_file', + id: file._id.toString(), + body: { + name: file.name, + userId: user._id.toString() + } + }); + } + }); + }) + .catch(reject); +}); diff --git a/src/api/drive/upload-from-url.ts b/src/api/drive/upload-from-url.ts new file mode 100644 index 0000000000..f96af0f266 --- /dev/null +++ b/src/api/drive/upload-from-url.ts @@ -0,0 +1,46 @@ +import * as URL from 'url'; +import { IDriveFile, validateFileName } from '../models/drive-file'; +import create from './add-file'; +import * as debug from 'debug'; +import * as tmp from 'tmp'; +import * as fs from 'fs'; +import * as request from 'request'; + +const log = debug('misskey:common:drive:upload_from_url'); + +export default async (url, user, folderId = null, uri = null): Promise => { + let name = URL.parse(url).pathname.split('/').pop(); + if (!validateFileName(name)) { + name = null; + } + + // Create temp file + const path = await new Promise((res: (string) => void, rej) => { + tmp.file((e, path) => { + if (e) return rej(e); + res(path); + }); + }); + + // write content at URL to temp file + await new Promise((res, rej) => { + const writable = fs.createWriteStream(path); + request(url) + .on('error', rej) + .on('end', () => { + writable.close(); + res(path); + }) + .pipe(writable) + .on('error', rej); + }); + + const driveFile = await create(user, path, name, null, folderId, false, uri); + + // clean-up + fs.unlink(path, (e) => { + if (e) log(e.stack); + }); + + return driveFile; +}; diff --git a/src/api/following/create.ts b/src/api/following/create.ts new file mode 100644 index 0000000000..353a6c8920 --- /dev/null +++ b/src/api/following/create.ts @@ -0,0 +1,82 @@ +import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; +import Following from '../../models/following'; +import FollowingLog from '../../models/following-log'; +import FollowedLog from '../../models/followed-log'; +import event from '../../publishers/stream'; +import notify from '../../publishers/notify'; +import context from '../../remote/activitypub/renderer/context'; +import renderFollow from '../../remote/activitypub/renderer/follow'; +import renderAccept from '../../remote/activitypub/renderer/accept'; +import { createHttp } from '../../queue'; + +export default async function(follower: IUser, followee: IUser, activity?) { + const following = await Following.insert({ + createdAt: new Date(), + followerId: follower._id, + followeeId: followee._id + }); + + //#region Increment following count + User.update({ _id: follower._id }, { + $inc: { + followingCount: 1 + } + }); + + FollowingLog.insert({ + createdAt: following.createdAt, + userId: follower._id, + count: follower.followingCount + 1 + }); + //#endregion + + //#region Increment followers count + User.update({ _id: followee._id }, { + $inc: { + followersCount: 1 + } + }); + FollowedLog.insert({ + createdAt: following.createdAt, + userId: followee._id, + count: followee.followersCount + 1 + }); + //#endregion + + // Publish follow event + if (isLocalUser(follower)) { + packUser(followee, follower).then(packed => event(follower._id, 'follow', packed)); + } + + // Publish followed event + if (isLocalUser(followee)) { + packUser(follower, followee).then(packed => event(followee._id, 'followed', packed)), + + // 通知を作成 + notify(followee._id, follower._id, 'follow'); + } + + if (isLocalUser(follower) && isRemoteUser(followee)) { + const content = renderFollow(follower, followee); + content['@context'] = context; + + createHttp({ + type: 'deliver', + user: follower, + content, + to: followee.account.inbox + }).save(); + } + + if (isRemoteUser(follower) && isLocalUser(followee)) { + const content = renderAccept(activity); + content['@context'] = context; + + createHttp({ + type: 'deliver', + user: followee, + content, + to: follower.account.inbox + }).save(); + } +} diff --git a/src/api/post/create.ts b/src/api/post/create.ts new file mode 100644 index 0000000000..f78bbe7521 --- /dev/null +++ b/src/api/post/create.ts @@ -0,0 +1,149 @@ +import parseAcct from '../acct/parse'; +import Post, { pack } from '../models/post'; +import User, { isLocalUser, isRemoteUser, IUser } from '../models/user'; +import stream from '../publishers/stream'; +import Following from '../models/following'; +import { createHttp } from '../queue'; +import renderNote from '../remote/activitypub/renderer/note'; +import renderCreate from '../remote/activitypub/renderer/create'; +import context from '../remote/activitypub/renderer/context'; + +export default async (user: IUser, post, reply, repost, atMentions) => { + post.mentions = []; + + function addMention(mentionee) { + // Reject if already added + if (post.mentions.some(x => x.equals(mentionee))) return; + + // Add mention + post.mentions.push(mentionee); + } + + if (reply) { + // Add mention + addMention(reply.userId); + post.replyId = reply._id; + post._reply = { userId: reply.userId }; + } else { + post.replyId = null; + post._reply = null; + } + + if (repost) { + if (post.text) { + // Add mention + addMention(repost.userId); + } + + post.repostId = repost._id; + post._repost = { userId: repost.userId }; + } else { + post.repostId = null; + post._repost = null; + } + + await Promise.all(atMentions.map(async mention => { + // Fetch mentioned user + // SELECT _id + const { _id } = await User + .findOne(parseAcct(mention), { _id: true }); + + // Add mention + addMention(_id); + })); + + const inserted = await Post.insert(post); + + User.update({ _id: user._id }, { + // Increment my posts count + $inc: { + postsCount: 1 + }, + + $set: { + latestPost: post._id + } + }); + + const postObj = await pack(inserted); + + // タイムラインへの投稿 + if (!post.channelId) { + // Publish event to myself's stream + stream(post.userId, 'post', postObj); + + // Fetch all followers + const followers = await Following.aggregate([{ + $lookup: { + from: 'users', + localField: 'followerId', + foreignField: '_id', + as: 'follower' + } + }, { + $match: { + followeeId: post.userId + } + }], { + _id: false + }); + + const note = await renderNote(user, post); + const content = renderCreate(note); + content['@context'] = context; + + Promise.all(followers.map(({ follower }) => { + if (isLocalUser(follower)) { + // Publish event to followers stream + stream(follower._id, 'post', postObj); + } else { + // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 + if (isLocalUser(user)) { + createHttp({ + type: 'deliver', + user, + content, + to: follower.account.inbox + }).save(); + } + } + })); + } + + // チャンネルへの投稿 + /* TODO + if (post.channelId) { + promises.push( + // Increment channel index(posts count) + Channel.update({ _id: post.channelId }, { + $inc: { + index: 1 + } + }), + + // Publish event to channel + promisedPostObj.then(postObj => { + publishChannelStream(post.channelId, 'post', postObj); + }), + + Promise.all([ + promisedPostObj, + + // Get channel watchers + ChannelWatching.find({ + channelId: post.channelId, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }) + ]).then(([postObj, watches]) => { + // チャンネルの視聴者(のタイムライン)に配信 + watches.forEach(w => { + stream(w.userId, 'post', postObj); + }); + }) + ); + }*/ + + return Promise.all(promises); + +}; diff --git a/src/api/post/distribute.ts b/src/api/post/distribute.ts new file mode 100644 index 0000000000..49c6eb22df --- /dev/null +++ b/src/api/post/distribute.ts @@ -0,0 +1,190 @@ +import Mute from '../models/mute'; +import Post, { pack } from '../models/post'; +import Watching from '../models/post-watching'; +import User from '../models/user'; +import stream from '../publishers/stream'; +import notify from '../publishers/notify'; +import pushSw from '../publishers/push-sw'; +import queue from '../queue'; +import watch from './watch'; + +export default async (user, mentions, post) => { + const promisedPostObj = pack(post); + const promises = [ + User.update({ _id: user._id }, { + // Increment my posts count + $inc: { + postsCount: 1 + }, + + $set: { + latestPost: post._id + } + }), + new Promise((resolve, reject) => queue.create('http', { + type: 'deliverPost', + id: post._id, + }).save(error => error ? reject(error) : resolve())), + ] as Array>; + + function addMention(promisedMentionee, reason) { + // Publish event + promises.push(promisedMentionee.then(mentionee => { + if (user._id.equals(mentionee)) { + return Promise.resolve(); + } + + return Promise.all([ + promisedPostObj, + Mute.find({ + muterId: mentionee, + deletedAt: { $exists: false } + }) + ]).then(([postObj, mentioneeMutes]) => { + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); + if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) { + stream(mentionee, reason, postObj); + pushSw(mentionee, reason, postObj); + } + }); + })); + } + + // If has in reply to post + if (post.replyId) { + promises.push( + // Increment replies count + Post.update({ _id: post.replyId }, { + $inc: { + repliesCount: 1 + } + }), + + // 自分自身へのリプライでない限りは通知を作成 + promisedPostObj.then(({ reply }) => { + return notify(reply.userId, user._id, 'reply', { + postId: post._id + }); + }), + + // Fetch watchers + Watching + .find({ + postId: post.replyId, + userId: { $ne: user._id }, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }, { + fields: { + userId: true + } + }) + .then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, 'reply', { + postId: post._id + }); + }); + }) + ); + + // Add mention + addMention(promisedPostObj.then(({ reply }) => reply.userId), 'reply'); + + // この投稿をWatchする + if (user.account.settings.autoWatch !== false) { + promises.push(promisedPostObj.then(({ reply }) => { + return watch(user._id, reply); + })); + } + } + + // If it is repost + if (post.repostId) { + const type = post.text ? 'quote' : 'repost'; + + promises.push( + promisedPostObj.then(({ repost }) => Promise.all([ + // Notify + notify(repost.userId, user._id, type, { + postId: post._id + }), + + // この投稿をWatchする + // TODO: ユーザーが「Repostしたときに自動でWatchする」設定を + // オフにしていた場合はしない + watch(user._id, repost) + ])), + + // Fetch watchers + Watching + .find({ + postId: post.repostId, + userId: { $ne: user._id }, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }, { + fields: { + userId: true + } + }) + .then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, type, { + postId: post._id + }); + }); + }) + ); + + // If it is quote repost + if (post.text) { + // Add mention + addMention(promisedPostObj.then(({ repost }) => repost.userId), 'quote'); + } else { + promises.push(promisedPostObj.then(postObj => { + // Publish event + if (!user._id.equals(postObj.repost.userId)) { + stream(postObj.repost.userId, 'repost', postObj); + } + })); + } + + // 今までで同じ投稿をRepostしているか + const existRepost = await Post.findOne({ + userId: user._id, + repostId: post.repostId, + _id: { + $ne: post._id + } + }); + + if (!existRepost) { + // Update repostee status + promises.push(Post.update({ _id: post.repostId }, { + $inc: { + repostCount: 1 + } + })); + } + } + + // Resolve all mentions + await promisedPostObj.then(({ reply, repost }) => Promise.all(mentions.map(async mention => { + // 既に言及されたユーザーに対する返信や引用repostの場合も無視 + if (reply && reply.userId.equals(mention)) return; + if (repost && repost.userId.equals(mention)) return; + + // Add mention + addMention(mention, 'mention'); + + // Create notification + await notify(mention, user._id, 'mention', { + postId: post._id + }); + }))); + + await Promise.all(promises); + + return promisedPostObj; +}; diff --git a/src/api/post/watch.ts b/src/api/post/watch.ts new file mode 100644 index 0000000000..61ea444430 --- /dev/null +++ b/src/api/post/watch.ts @@ -0,0 +1,26 @@ +import * as mongodb from 'mongodb'; +import Watching from '../models/post-watching'; + +export default async (me: mongodb.ObjectID, post: object) => { + // 自分の投稿はwatchできない + if (me.equals((post as any).userId)) { + return; + } + + // if watching now + const exist = await Watching.findOne({ + postId: (post as any)._id, + userId: me, + deletedAt: { $exists: false } + }); + + if (exist !== null) { + return; + } + + await Watching.insert({ + createdAt: new Date(), + postId: (post as any)._id, + userId: me + }); +}; diff --git a/src/drive/add-file.ts b/src/drive/add-file.ts deleted file mode 100644 index 24eb5208d5..0000000000 --- a/src/drive/add-file.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { Buffer } from 'buffer'; -import * as fs from 'fs'; -import * as tmp from 'tmp'; -import * as stream from 'stream'; - -import * as mongodb from 'mongodb'; -import * as crypto from 'crypto'; -import * as _gm from 'gm'; -import * as debug from 'debug'; -import fileType = require('file-type'); -import prominence = require('prominence'); - -import DriveFile, { IMetadata, getGridFSBucket } from '../models/drive-file'; -import DriveFolder from '../models/drive-folder'; -import { pack } from '../models/drive-file'; -import event, { publishDriveStream } from '../publishers/stream'; -import getAcct from '../acct/render'; -import config from '../config'; - -const gm = _gm.subClass({ - imageMagick: true -}); - -const log = debug('misskey:drive:add-file'); - -const tmpFile = (): Promise => new Promise((resolve, reject) => { - tmp.file((e, path) => { - if (e) return reject(e); - resolve(path); - }); -}); - -const addToGridFS = (name: string, readable: stream.Readable, type: string, metadata: any): Promise => - getGridFSBucket() - .then(bucket => new Promise((resolve, reject) => { - const writeStream = bucket.openUploadStream(name, { contentType: type, metadata }); - writeStream.once('finish', (doc) => { resolve(doc); }); - writeStream.on('error', reject); - readable.pipe(writeStream); - })); - -const addFile = async ( - user: any, - path: string, - name: string = null, - comment: string = null, - folderId: mongodb.ObjectID = null, - force: boolean = false, - uri: string = null -) => { - log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`); - - // Calculate hash, get content type and get file size - const [hash, [mime, ext], size] = await Promise.all([ - // hash - ((): Promise => new Promise((res, rej) => { - const readable = fs.createReadStream(path); - const hash = crypto.createHash('md5'); - const chunks = []; - readable - .on('error', rej) - .pipe(hash) - .on('error', rej) - .on('data', (chunk) => chunks.push(chunk)) - .on('end', () => { - const buffer = Buffer.concat(chunks); - res(buffer.toString('hex')); - }); - }))(), - // mime - ((): Promise<[string, string | null]> => new Promise((res, rej) => { - const readable = fs.createReadStream(path); - readable - .on('error', rej) - .once('data', (buffer: Buffer) => { - readable.destroy(); - const type = fileType(buffer); - if (type) { - return res([type.mime, type.ext]); - } else { - // 種類が同定できなかったら application/octet-stream にする - return res(['application/octet-stream', null]); - } - }); - }))(), - // size - ((): Promise => new Promise((res, rej) => { - fs.stat(path, (err, stats) => { - if (err) return rej(err); - res(stats.size); - }); - }))() - ]); - - log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`); - - // detect name - const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled'); - - if (!force) { - // Check if there is a file with the same hash - const much = await DriveFile.findOne({ - md5: hash, - 'metadata.userId': user._id - }); - - if (much !== null) { - log('file with same hash is found'); - return much; - } else { - log('file with same hash is not found'); - } - } - - const [wh, averageColor, folder] = await Promise.all([ - // Width and height (when image) - (async () => { - // 画像かどうか - if (!/^image\/.*$/.test(mime)) { - return null; - } - - const imageType = mime.split('/')[1]; - - // 画像でもPNGかJPEGかGIFでないならスキップ - if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') { - return null; - } - - log('calculate image width and height...'); - - // Calculate width and height - const g = gm(fs.createReadStream(path), name); - const size = await prominence(g).size(); - - log(`image width and height is calculated: ${size.width}, ${size.height}`); - - return [size.width, size.height]; - })(), - // average color (when image) - (async () => { - // 画像かどうか - if (!/^image\/.*$/.test(mime)) { - return null; - } - - const imageType = mime.split('/')[1]; - - // 画像でもPNGかJPEGでないならスキップ - if (imageType != 'png' && imageType != 'jpeg') { - return null; - } - - log('calculate average color...'); - - const buffer = await prominence(gm(fs.createReadStream(path), name) - .setFormat('ppm') - .resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック - .toBuffer(); - - const r = buffer.readUInt8(buffer.length - 3); - const g = buffer.readUInt8(buffer.length - 2); - const b = buffer.readUInt8(buffer.length - 1); - - log(`average color is calculated: ${r}, ${g}, ${b}`); - - return [r, g, b]; - })(), - // folder - (async () => { - if (!folderId) { - return null; - } - const driveFolder = await DriveFolder.findOne({ - _id: folderId, - userId: user._id - }); - if (!driveFolder) { - throw 'folder-not-found'; - } - return driveFolder; - })(), - // usage checker - (async () => { - // Calculate drive usage - const usage = await DriveFile - .aggregate([{ - $match: { 'metadata.userId': user._id } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then((aggregates: any[]) => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - log(`drive usage is ${usage}`); - - // If usage limit exceeded - if (usage + size > user.driveCapacity) { - throw 'no-free-space'; - } - })() - ]); - - const readable = fs.createReadStream(path); - - const properties = {}; - - if (wh) { - properties['width'] = wh[0]; - properties['height'] = wh[1]; - } - - if (averageColor) { - properties['avgColor'] = averageColor; - } - - const metadata = { - userId: user._id, - folderId: folder !== null ? folder._id : null, - comment: comment, - properties: properties - } as IMetadata; - - if (uri !== null) { - metadata.uri = uri; - } - - return addToGridFS(detectedName, readable, mime, metadata); -}; - -/** - * Add file to drive - * - * @param user User who wish to add file - * @param file File path or readableStream - * @param comment Comment - * @param type File type - * @param folderId Folder ID - * @param force If set to true, forcibly upload the file even if there is a file with the same hash. - * @return Object that represents added file - */ -export default (user: any, file: string | stream.Readable, ...args) => new Promise((resolve, reject) => { - // Get file path - new Promise((res: (v: [string, boolean]) => void, rej) => { - if (typeof file === 'string') { - res([file, false]); - return; - } - if (typeof file === 'object' && typeof file.read === 'function') { - tmpFile() - .then(path => { - const readable: stream.Readable = file; - const writable = fs.createWriteStream(path); - readable - .on('error', rej) - .on('end', () => { - res([path, true]); - }) - .pipe(writable) - .on('error', rej); - }) - .catch(rej); - } - rej(new Error('un-compatible file.')); - }) - .then(([path, shouldCleanup]): Promise => new Promise((res, rej) => { - addFile(user, path, ...args) - .then(file => { - res(file); - if (shouldCleanup) { - fs.unlink(path, (e) => { - if (e) log(e.stack); - }); - } - }) - .catch(rej); - })) - .then(file => { - log(`drive file has been created ${file._id}`); - resolve(file); - - pack(file).then(serializedFile => { - // Publish drive_file_created event - event(user._id, 'drive_file_created', serializedFile); - publishDriveStream(user._id, 'file_created', serializedFile); - - // Register to search database - if (config.elasticsearch.enable) { - const es = require('../db/elasticsearch'); - es.index({ - index: 'misskey', - type: 'drive_file', - id: file._id.toString(), - body: { - name: file.name, - userId: user._id.toString() - } - }); - } - }); - }) - .catch(reject); -}); diff --git a/src/drive/upload-from-url.ts b/src/drive/upload-from-url.ts deleted file mode 100644 index f96af0f266..0000000000 --- a/src/drive/upload-from-url.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as URL from 'url'; -import { IDriveFile, validateFileName } from '../models/drive-file'; -import create from './add-file'; -import * as debug from 'debug'; -import * as tmp from 'tmp'; -import * as fs from 'fs'; -import * as request from 'request'; - -const log = debug('misskey:common:drive:upload_from_url'); - -export default async (url, user, folderId = null, uri = null): Promise => { - let name = URL.parse(url).pathname.split('/').pop(); - if (!validateFileName(name)) { - name = null; - } - - // Create temp file - const path = await new Promise((res: (string) => void, rej) => { - tmp.file((e, path) => { - if (e) return rej(e); - res(path); - }); - }); - - // write content at URL to temp file - await new Promise((res, rej) => { - const writable = fs.createWriteStream(path); - request(url) - .on('error', rej) - .on('end', () => { - writable.close(); - res(path); - }) - .pipe(writable) - .on('error', rej); - }); - - const driveFile = await create(user, path, name, null, folderId, false, uri); - - // clean-up - fs.unlink(path, (e) => { - if (e) log(e.stack); - }); - - return driveFile; -}; diff --git a/src/post/create.ts b/src/post/create.ts deleted file mode 100644 index f78bbe7521..0000000000 --- a/src/post/create.ts +++ /dev/null @@ -1,149 +0,0 @@ -import parseAcct from '../acct/parse'; -import Post, { pack } from '../models/post'; -import User, { isLocalUser, isRemoteUser, IUser } from '../models/user'; -import stream from '../publishers/stream'; -import Following from '../models/following'; -import { createHttp } from '../queue'; -import renderNote from '../remote/activitypub/renderer/note'; -import renderCreate from '../remote/activitypub/renderer/create'; -import context from '../remote/activitypub/renderer/context'; - -export default async (user: IUser, post, reply, repost, atMentions) => { - post.mentions = []; - - function addMention(mentionee) { - // Reject if already added - if (post.mentions.some(x => x.equals(mentionee))) return; - - // Add mention - post.mentions.push(mentionee); - } - - if (reply) { - // Add mention - addMention(reply.userId); - post.replyId = reply._id; - post._reply = { userId: reply.userId }; - } else { - post.replyId = null; - post._reply = null; - } - - if (repost) { - if (post.text) { - // Add mention - addMention(repost.userId); - } - - post.repostId = repost._id; - post._repost = { userId: repost.userId }; - } else { - post.repostId = null; - post._repost = null; - } - - await Promise.all(atMentions.map(async mention => { - // Fetch mentioned user - // SELECT _id - const { _id } = await User - .findOne(parseAcct(mention), { _id: true }); - - // Add mention - addMention(_id); - })); - - const inserted = await Post.insert(post); - - User.update({ _id: user._id }, { - // Increment my posts count - $inc: { - postsCount: 1 - }, - - $set: { - latestPost: post._id - } - }); - - const postObj = await pack(inserted); - - // タイムラインへの投稿 - if (!post.channelId) { - // Publish event to myself's stream - stream(post.userId, 'post', postObj); - - // Fetch all followers - const followers = await Following.aggregate([{ - $lookup: { - from: 'users', - localField: 'followerId', - foreignField: '_id', - as: 'follower' - } - }, { - $match: { - followeeId: post.userId - } - }], { - _id: false - }); - - const note = await renderNote(user, post); - const content = renderCreate(note); - content['@context'] = context; - - Promise.all(followers.map(({ follower }) => { - if (isLocalUser(follower)) { - // Publish event to followers stream - stream(follower._id, 'post', postObj); - } else { - // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 - if (isLocalUser(user)) { - createHttp({ - type: 'deliver', - user, - content, - to: follower.account.inbox - }).save(); - } - } - })); - } - - // チャンネルへの投稿 - /* TODO - if (post.channelId) { - promises.push( - // Increment channel index(posts count) - Channel.update({ _id: post.channelId }, { - $inc: { - index: 1 - } - }), - - // Publish event to channel - promisedPostObj.then(postObj => { - publishChannelStream(post.channelId, 'post', postObj); - }), - - Promise.all([ - promisedPostObj, - - // Get channel watchers - ChannelWatching.find({ - channelId: post.channelId, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }) - ]).then(([postObj, watches]) => { - // チャンネルの視聴者(のタイムライン)に配信 - watches.forEach(w => { - stream(w.userId, 'post', postObj); - }); - }) - ); - }*/ - - return Promise.all(promises); - -}; diff --git a/src/post/distribute.ts b/src/post/distribute.ts deleted file mode 100644 index 49c6eb22df..0000000000 --- a/src/post/distribute.ts +++ /dev/null @@ -1,190 +0,0 @@ -import Mute from '../models/mute'; -import Post, { pack } from '../models/post'; -import Watching from '../models/post-watching'; -import User from '../models/user'; -import stream from '../publishers/stream'; -import notify from '../publishers/notify'; -import pushSw from '../publishers/push-sw'; -import queue from '../queue'; -import watch from './watch'; - -export default async (user, mentions, post) => { - const promisedPostObj = pack(post); - const promises = [ - User.update({ _id: user._id }, { - // Increment my posts count - $inc: { - postsCount: 1 - }, - - $set: { - latestPost: post._id - } - }), - new Promise((resolve, reject) => queue.create('http', { - type: 'deliverPost', - id: post._id, - }).save(error => error ? reject(error) : resolve())), - ] as Array>; - - function addMention(promisedMentionee, reason) { - // Publish event - promises.push(promisedMentionee.then(mentionee => { - if (user._id.equals(mentionee)) { - return Promise.resolve(); - } - - return Promise.all([ - promisedPostObj, - Mute.find({ - muterId: mentionee, - deletedAt: { $exists: false } - }) - ]).then(([postObj, mentioneeMutes]) => { - const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); - if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) { - stream(mentionee, reason, postObj); - pushSw(mentionee, reason, postObj); - } - }); - })); - } - - // If has in reply to post - if (post.replyId) { - promises.push( - // Increment replies count - Post.update({ _id: post.replyId }, { - $inc: { - repliesCount: 1 - } - }), - - // 自分自身へのリプライでない限りは通知を作成 - promisedPostObj.then(({ reply }) => { - return notify(reply.userId, user._id, 'reply', { - postId: post._id - }); - }), - - // Fetch watchers - Watching - .find({ - postId: post.replyId, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.userId, user._id, 'reply', { - postId: post._id - }); - }); - }) - ); - - // Add mention - addMention(promisedPostObj.then(({ reply }) => reply.userId), 'reply'); - - // この投稿をWatchする - if (user.account.settings.autoWatch !== false) { - promises.push(promisedPostObj.then(({ reply }) => { - return watch(user._id, reply); - })); - } - } - - // If it is repost - if (post.repostId) { - const type = post.text ? 'quote' : 'repost'; - - promises.push( - promisedPostObj.then(({ repost }) => Promise.all([ - // Notify - notify(repost.userId, user._id, type, { - postId: post._id - }), - - // この投稿をWatchする - // TODO: ユーザーが「Repostしたときに自動でWatchする」設定を - // オフにしていた場合はしない - watch(user._id, repost) - ])), - - // Fetch watchers - Watching - .find({ - postId: post.repostId, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.userId, user._id, type, { - postId: post._id - }); - }); - }) - ); - - // If it is quote repost - if (post.text) { - // Add mention - addMention(promisedPostObj.then(({ repost }) => repost.userId), 'quote'); - } else { - promises.push(promisedPostObj.then(postObj => { - // Publish event - if (!user._id.equals(postObj.repost.userId)) { - stream(postObj.repost.userId, 'repost', postObj); - } - })); - } - - // 今までで同じ投稿をRepostしているか - const existRepost = await Post.findOne({ - userId: user._id, - repostId: post.repostId, - _id: { - $ne: post._id - } - }); - - if (!existRepost) { - // Update repostee status - promises.push(Post.update({ _id: post.repostId }, { - $inc: { - repostCount: 1 - } - })); - } - } - - // Resolve all mentions - await promisedPostObj.then(({ reply, repost }) => Promise.all(mentions.map(async mention => { - // 既に言及されたユーザーに対する返信や引用repostの場合も無視 - if (reply && reply.userId.equals(mention)) return; - if (repost && repost.userId.equals(mention)) return; - - // Add mention - addMention(mention, 'mention'); - - // Create notification - await notify(mention, user._id, 'mention', { - postId: post._id - }); - }))); - - await Promise.all(promises); - - return promisedPostObj; -}; diff --git a/src/post/watch.ts b/src/post/watch.ts deleted file mode 100644 index 61ea444430..0000000000 --- a/src/post/watch.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as mongodb from 'mongodb'; -import Watching from '../models/post-watching'; - -export default async (me: mongodb.ObjectID, post: object) => { - // 自分の投稿はwatchできない - if (me.equals((post as any).userId)) { - return; - } - - // if watching now - const exist = await Watching.findOne({ - postId: (post as any)._id, - userId: me, - deletedAt: { $exists: false } - }); - - if (exist !== null) { - return; - } - - await Watching.insert({ - createdAt: new Date(), - postId: (post as any)._id, - userId: me - }); -}; diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts index d3d5f2246f..801a3612a7 100644 --- a/src/queue/processors/http/unfollow.ts +++ b/src/queue/processors/http/unfollow.ts @@ -1,56 +1,63 @@ -import FollowedLog from '../../models/followed-log'; -import Following from '../../models/following'; -import FollowingLog from '../../models/following-log'; -import User, { isRemoteUser, pack as packUser } from '../../models/user'; -import stream from '../../publishers/stream'; -import renderFollow from '../../remote/activitypub/renderer/follow'; -import renderUndo from '../../remote/activitypub/renderer/undo'; -import context from '../../remote/activitypub/renderer/context'; -import request from '../../remote/request'; +import FollowedLog from '../../../models/followed-log'; +import Following from '../../../models/following'; +import FollowingLog from '../../../models/following-log'; +import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user'; +import stream from '../../../publishers/stream'; +import renderFollow from '../../../remote/activitypub/renderer/follow'; +import renderUndo from '../../../remote/activitypub/renderer/undo'; +import context from '../../../remote/activitypub/renderer/context'; +import request from '../../../remote/request'; +import Logger from '../../../utils/logger'; export default async ({ data }) => { - // Delete following - const following = await Following.findOneAndDelete({ _id: data.id }); + const following = await Following.findOne({ _id: data.id }); if (following === null) { return; } - const promisedFollower = User.findOne({ _id: following.followerId }); - const promisedFollowee = User.findOne({ _id: following.followeeId }); - - await Promise.all([ - // Decrement following count - User.update({ _id: following.followerId }, { $inc: { followingCount: -1 } }), - promisedFollower.then(({ followingCount }) => FollowingLog.insert({ - createdAt: new Date(), - userId: following.followerId, - count: followingCount - 1 - })), - - // Decrement followers count - User.update({ _id: following.followeeId }, { $inc: { followersCount: -1 } }), - promisedFollowee.then(({ followersCount }) => FollowedLog.insert({ - createdAt: new Date(), - userId: following.followeeId, - count: followersCount - 1 - })), - - // Publish follow event - Promise.all([promisedFollower, promisedFollowee]).then(async ([follower, followee]) => { - if (isRemoteUser(follower)) { - return; - } + const [follower, followee] = await Promise.all([ + User.findOne({ _id: following.followerId }), + User.findOne({ _id: following.followeeId }) + ]); - const promisedPackedUser = packUser(followee, follower); + if (isLocalUser(follower) && isRemoteUser(followee)) { + const undo = renderUndo(renderFollow(follower, followee)); + undo['@context'] = context; - if (isRemoteUser(followee)) { - const undo = renderUndo(renderFollow(follower, followee)); - undo['@context'] = context; + await request(follower, followee.account.inbox, undo); + } - await request(follower, followee.account.inbox, undo); - } + try { + await Promise.all([ + // Delete following + Following.findOneAndDelete({ _id: data.id }), + + // Decrement following count + User.update({ _id: follower._id }, { $inc: { followingCount: -1 } }), + FollowingLog.insert({ + createdAt: new Date(), + userId: follower._id, + count: follower.followingCount - 1 + }), + + // Decrement followers count + User.update({ _id: followee._id }, { $inc: { followersCount: -1 } }), + FollowedLog.insert({ + createdAt: new Date(), + userId: followee._id, + count: followee.followersCount - 1 + }) + ]); + + if (isLocalUser(follower)) { + return; + } + + const promisedPackedUser = packUser(followee, follower); - stream(follower._id, 'unfollow', promisedPackedUser); - }) - ]); + // Publish follow event + stream(follower._id, 'unfollow', promisedPackedUser); + } catch (error) { + Logger.error(error.toString()); + } }; diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts index c1a30ce7d0..7ee9f8dfb7 100644 --- a/src/remote/activitypub/act/create.ts +++ b/src/remote/activitypub/act/create.ts @@ -4,10 +4,10 @@ const createDOMPurify = require('dompurify'); import Resolver from '../resolver'; import DriveFile from '../../../models/drive-file'; import Post from '../../../models/post'; -import uploadFromUrl from '../../../drive/upload-from-url'; -import createPost from '../../../post/create'; +import uploadFromUrl from '../../../api/drive/upload-from-url'; +import createPost from '../../../api/post/create'; -export default async (resolver: Resolver, actor, activity): Promise => { +export default async (actor, activity): Promise => { if ('actor' in activity && actor.account.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -31,6 +31,8 @@ export default async (resolver: Resolver, actor, activity): Promise => { throw new Error(`already registered: ${uri}`); } + const resolver = new Resolver(); + const object = await resolver.resolve(activity); switch (object.type) { diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts index 23fa41df8e..dc173a0acb 100644 --- a/src/remote/activitypub/act/follow.ts +++ b/src/remote/activitypub/act/follow.ts @@ -1,15 +1,9 @@ -import { MongoError } from 'mongodb'; import parseAcct from '../../../acct/parse'; -import Following, { IFollowing } from '../../../models/following'; import User from '../../../models/user'; import config from '../../../config'; -import queue from '../../../queue'; -import context from '../renderer/context'; -import renderAccept from '../renderer/accept'; -import request from '../../request'; -import Resolver from '../resolver'; +import follow from '../../../api/following/create'; -export default async (resolver: Resolver, actor, activity, distribute) => { +export default async (actor, activity): Promise => { const prefix = config.url + '/@'; const id = activity.object.id || activity.object; @@ -27,52 +21,5 @@ export default async (resolver: Resolver, actor, activity, distribute) => { throw new Error(); } - if (!distribute) { - const { _id } = await Following.findOne({ - followerId: actor._id, - followeeId: followee._id - }); - - return { - resolver, - object: { $ref: 'following', $id: _id } - }; - } - - const promisedFollowing = Following.insert({ - createdAt: new Date(), - followerId: actor._id, - followeeId: followee._id - }).then(following => new Promise((resolve, reject) => { - queue.create('http', { - type: 'follow', - following: following._id - }).save(error => { - if (error) { - reject(error); - } else { - resolve(following); - } - }); - }) as Promise, async error => { - // duplicate key error - if (error instanceof MongoError && error.code === 11000) { - return Following.findOne({ - followerId: actor._id, - followeeId: followee._id - }); - } - - throw error; - }); - - const accept = renderAccept(activity); - accept['@context'] = context; - - await request(followee, actor.account.inbox, accept); - - return promisedFollowing.then(({ _id }) => ({ - resolver, - object: { $ref: 'following', $id: _id } - })); + await follow(actor, followee, activity); }; -- cgit v1.2.3-freya From 0ac695edbda10231ee70f0a0debedb4c8f708954 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 5 Apr 2018 00:01:01 +0900 Subject: wip --- src/queue/processors/http/follow.ts | 69 ------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 src/queue/processors/http/follow.ts (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/follow.ts b/src/queue/processors/http/follow.ts deleted file mode 100644 index 8bf890efbc..0000000000 --- a/src/queue/processors/http/follow.ts +++ /dev/null @@ -1,69 +0,0 @@ -import User, { isLocalUser, pack as packUser } from '../../models/user'; -import Following from '../../models/following'; -import FollowingLog from '../../models/following-log'; -import FollowedLog from '../../models/followed-log'; -import event from '../../publishers/stream'; -import notify from '../../publishers/notify'; -import context from '../../remote/activitypub/renderer/context'; -import render from '../../remote/activitypub/renderer/follow'; -import request from '../../remote/request'; - -export default ({ data }) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => { - const promisedFollower = User.findOne({ _id: followerId }); - const promisedFollowee = User.findOne({ _id: followeeId }); - - return Promise.all([ - // Increment following count - User.update(followerId, { - $inc: { - followingCount: 1 - } - }), - - promisedFollower.then(({ followingCount }) => FollowingLog.insert({ - createdAt: data.following.createdAt, - userId: followerId, - count: followingCount + 1 - })), - - // Increment followers count - User.update({ _id: followeeId }, { - $inc: { - followersCount: 1 - } - }), - - promisedFollowee.then(({ followersCount }) => FollowedLog.insert({ - createdAt: data.following.createdAt, - userId: followerId, - count: followersCount + 1 - })), - - // Notify - promisedFollowee.then(followee => followee.host === null ? - notify(followeeId, followerId, 'follow') : null), - - // Publish follow event - Promise.all([promisedFollower, promisedFollowee]).then(([follower, followee]) => { - let followerEvent; - let followeeEvent; - - if (isLocalUser(follower)) { - followerEvent = packUser(followee, follower) - .then(packed => event(follower._id, 'follow', packed)); - } - - if (isLocalUser(followee)) { - followeeEvent = packUser(follower, followee) - .then(packed => event(followee._id, 'followed', packed)); - } else if (isLocalUser(follower)) { - const rendered = render(follower, followee); - rendered['@context'] = context; - - followeeEvent = request(follower, followee.account.inbox, rendered); - } - - return Promise.all([followerEvent, followeeEvent]); - }) - ]); -}); -- cgit v1.2.3-freya From e330ac1934516807757afe2d2760fa21b27006e6 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Thu, 5 Apr 2018 01:04:44 +0900 Subject: Let unhandled rejection handler handle rejections in jobs --- src/queue/processors/db/delete-post-dependents.ts | 4 +- src/queue/processors/db/index.ts | 2 +- src/queue/processors/http/deliver-post.ts | 28 +++++--- src/queue/processors/http/follow.ts | 79 +++++++++++----------- src/queue/processors/http/index.ts | 2 +- src/queue/processors/http/perform-activitypub.ts | 5 +- src/queue/processors/http/process-inbox.ts | 51 +++++++------- src/queue/processors/http/report-github-failure.ts | 39 ++++++----- src/queue/processors/http/unfollow.ts | 31 +++++---- 9 files changed, 132 insertions(+), 109 deletions(-) (limited to 'src/queue/processors') diff --git a/src/queue/processors/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts index 6de21eb053..fb6617e952 100644 --- a/src/queue/processors/db/delete-post-dependents.ts +++ b/src/queue/processors/db/delete-post-dependents.ts @@ -5,7 +5,7 @@ import PostReaction from '../../../models/post-reaction'; import PostWatching from '../../../models/post-watching'; import Post from '../../../models/post'; -export default async ({ data }) => Promise.all([ +export default ({ data }, done) => Promise.all([ Favorite.remove({ postId: data._id }), Notification.remove({ postId: data._id }), PollVote.remove({ postId: data._id }), @@ -19,4 +19,4 @@ export default async ({ data }) => Promise.all([ }), Post.remove({ repostId: data._id }) ])) -]); +]).then(() => done(), done); diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts index 75838c099b..468ec442ac 100644 --- a/src/queue/processors/db/index.ts +++ b/src/queue/processors/db/index.ts @@ -4,4 +4,4 @@ const handlers = { deletePostDependents }; -export default (job, done) => handlers[job.data.type](job).then(() => done(), done); +export default (job, done) => handlers[job.data.type](job, done); diff --git a/src/queue/processors/http/deliver-post.ts b/src/queue/processors/http/deliver-post.ts index e743fc5f68..8107c8bf74 100644 --- a/src/queue/processors/http/deliver-post.ts +++ b/src/queue/processors/http/deliver-post.ts @@ -5,17 +5,23 @@ import renderCreate from '../../../remote/activitypub/renderer/create'; import renderNote from '../../../remote/activitypub/renderer/note'; import request from '../../../remote/request'; -export default async ({ data }) => { - const promisedTo = User.findOne({ _id: data.toId }) as Promise; - const [from, post] = await Promise.all([ - User.findOne({ _id: data.fromId }), - Post.findOne({ _id: data.postId }) - ]); - const note = await renderNote(from, post); - const to = await promisedTo; - const create = renderCreate(note); +export default async ({ data }, done) => { + try { + const promisedTo = User.findOne({ _id: data.toId }) as Promise; + const [from, post] = await Promise.all([ + User.findOne({ _id: data.fromId }), + Post.findOne({ _id: data.postId }) + ]); + const note = await renderNote(from, post); + const to = await promisedTo; + const create = renderCreate(note); - create['@context'] = context; + create['@context'] = context; - return request(from, to.account.inbox, create); + await request(from, to.account.inbox, create); + } catch (error) { + done(error); + } + + done(); }; diff --git a/src/queue/processors/http/follow.ts b/src/queue/processors/http/follow.ts index 4cb72828e7..ba1cc31186 100644 --- a/src/queue/processors/http/follow.ts +++ b/src/queue/processors/http/follow.ts @@ -7,10 +7,8 @@ import notify from '../../../publishers/notify'; import context from '../../../remote/activitypub/renderer/context'; import render from '../../../remote/activitypub/renderer/follow'; import request from '../../../remote/request'; -import Logger from '../../../utils/logger'; -export default async ({ data }) => { - const { followerId, followeeId } = await Following.findOne({ _id: data.following }); +export default ({ data }, done) => Following.findOne({ _id: data.following }).then(async ({ followerId, followeeId }) => { const [follower, followee] = await Promise.all([ User.findOne({ _id: followerId }), User.findOne({ _id: followeeId }) @@ -23,47 +21,46 @@ export default async ({ data }) => { await request(follower, followee.account.inbox, rendered); } - try { - await Promise.all([ - // Increment following count - User.update(followerId, { - $inc: { - followingCount: 1 - } - }), + return [follower, followee]; +}).then(([follower, followee]) => Promise.all([ + // Increment following count + User.update(follower._id, { + $inc: { + followingCount: 1 + } + }), - FollowingLog.insert({ - createdAt: data.following.createdAt, - userId: followerId, - count: follower.followingCount + 1 - }), + FollowingLog.insert({ + createdAt: data.following.createdAt, + userId: follower._id, + count: follower.followingCount + 1 + }), - // Increment followers count - User.update({ _id: followeeId }, { - $inc: { - followersCount: 1 - } - }), + // Increment followers count + User.update({ _id: followee._id }, { + $inc: { + followersCount: 1 + } + }), - FollowedLog.insert({ - createdAt: data.following.createdAt, - userId: followerId, - count: followee.followersCount + 1 - }), + FollowedLog.insert({ + createdAt: data.following.createdAt, + userId: follower._id, + count: followee.followersCount + 1 + }), - // Publish follow event - isLocalUser(follower) && packUser(followee, follower) - .then(packed => event(follower._id, 'follow', packed)), + // Publish follow event + isLocalUser(follower) && packUser(followee, follower) + .then(packed => event(follower._id, 'follow', packed)), - isLocalUser(followee) && Promise.all([ - packUser(follower, followee) - .then(packed => event(followee._id, 'followed', packed)), + isLocalUser(followee) && Promise.all([ + packUser(follower, followee) + .then(packed => event(followee._id, 'followed', packed)), - // Notify - isLocalUser(followee) && notify(followeeId, followerId, 'follow') - ]) - ]); - } catch (error) { - Logger.error(error.toString()); - } -}; + // Notify + isLocalUser(followee) && notify(followee._id, follower._id, 'follow') + ]) +]).then(() => done(), error => { + done(); + throw error; +}), done); diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts index 8f9aa717c3..0ea79305c6 100644 --- a/src/queue/processors/http/index.ts +++ b/src/queue/processors/http/index.ts @@ -14,4 +14,4 @@ const handlers = { unfollow }; -export default (job, done) => handlers[job.data.type](job).then(() => done(), done); +export default (job, done) => handlers[job.data.type](job, done); diff --git a/src/queue/processors/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts index 7b84400d5c..ae70c0f0be 100644 --- a/src/queue/processors/http/perform-activitypub.ts +++ b/src/queue/processors/http/perform-activitypub.ts @@ -2,6 +2,7 @@ import User from '../../../models/user'; import act from '../../../remote/activitypub/act'; import Resolver from '../../../remote/activitypub/resolver'; -export default ({ data }) => User.findOne({ _id: data.actor }) +export default ({ data }, done) => User.findOne({ _id: data.actor }) .then(actor => act(new Resolver(), actor, data.outbox)) - .then(Promise.all); + .then(Promise.all) + .then(() => done(), done); diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index de1dbd2f98..88fbb97377 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -5,35 +5,40 @@ import act from '../../../remote/activitypub/act'; import resolvePerson from '../../../remote/activitypub/resolve-person'; import Resolver from '../../../remote/activitypub/resolver'; -export default async ({ data }): Promise => { - const keyIdLower = data.signature.keyId.toLowerCase(); - let user; +export default async ({ data }, done) => { + try { + const keyIdLower = data.signature.keyId.toLowerCase(); + let user; - if (keyIdLower.startsWith('acct:')) { - const { username, host } = parseAcct(keyIdLower.slice('acct:'.length)); - if (host === null) { - throw 'request was made by local user'; - } + if (keyIdLower.startsWith('acct:')) { + const { username, host } = parseAcct(keyIdLower.slice('acct:'.length)); + if (host === null) { + done(); + return; + } - user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser; - } else { - user = await User.findOne({ - host: { $ne: null }, - 'account.publicKey.id': data.signature.keyId - }) as IRemoteUser; + user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser; + } else { + user = await User.findOne({ + host: { $ne: null }, + 'account.publicKey.id': data.signature.keyId + }) as IRemoteUser; - if (user === null) { - user = await resolvePerson(data.signature.keyId); + if (user === null) { + user = await resolvePerson(data.signature.keyId); + } } - } - if (user === null) { - throw 'failed to resolve user'; - } + if (user === null || !verifySignature(data.signature, user.account.publicKey.publicKeyPem)) { + done(); + return; + } - if (!verifySignature(data.signature, user.account.publicKey.publicKeyPem)) { - throw 'signature verification failed'; + await Promise.all(await act(new Resolver(), user, data.inbox, true)); + } catch (error) { + done(error); + return; } - await Promise.all(await act(new Resolver(), user, data.inbox, true)); + done(); }; diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts index 21683ba3c2..af9659bdac 100644 --- a/src/queue/processors/http/report-github-failure.ts +++ b/src/queue/processors/http/report-github-failure.ts @@ -2,23 +2,30 @@ import * as request from 'request-promise-native'; import User from '../../../models/user'; const createPost = require('../../../server/api/endpoints/posts/create'); -export default async ({ data }) => { - const asyncBot = User.findOne({ _id: data.userId }); +export default async ({ data }, done) => { + try { + const asyncBot = User.findOne({ _id: data.userId }); - // Fetch parent status - const parentStatuses = await request({ - url: `${data.parentUrl}/statuses`, - headers: { - 'User-Agent': 'misskey' - }, - json: true - }); + // Fetch parent status + const parentStatuses = await request({ + url: `${data.parentUrl}/statuses`, + headers: { + 'User-Agent': 'misskey' + }, + json: true + }); - const parentState = parentStatuses[0].state; - const stillFailed = parentState == 'failure' || parentState == 'error'; - const text = stillFailed ? - `**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` : - `**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`; + const parentState = parentStatuses[0].state; + const stillFailed = parentState == 'failure' || parentState == 'error'; + const text = stillFailed ? + `**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` : + `**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`; - createPost({ text }, await asyncBot); + createPost({ text }, await asyncBot); + } catch (error) { + done(error); + return; + } + + done(); }; diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts index 801a3612a7..dc50e946c9 100644 --- a/src/queue/processors/http/unfollow.ts +++ b/src/queue/processors/http/unfollow.ts @@ -7,24 +7,31 @@ import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderUndo from '../../../remote/activitypub/renderer/undo'; import context from '../../../remote/activitypub/renderer/context'; import request from '../../../remote/request'; -import Logger from '../../../utils/logger'; -export default async ({ data }) => { +export default async ({ data }, done) => { const following = await Following.findOne({ _id: data.id }); if (following === null) { + done(); return; } - const [follower, followee] = await Promise.all([ - User.findOne({ _id: following.followerId }), - User.findOne({ _id: following.followeeId }) - ]); + let follower, followee; - if (isLocalUser(follower) && isRemoteUser(followee)) { - const undo = renderUndo(renderFollow(follower, followee)); - undo['@context'] = context; + try { + [follower, followee] = await Promise.all([ + User.findOne({ _id: following.followerId }), + User.findOne({ _id: following.followeeId }) + ]); - await request(follower, followee.account.inbox, undo); + if (isLocalUser(follower) && isRemoteUser(followee)) { + const undo = renderUndo(renderFollow(follower, followee)); + undo['@context'] = context; + + await request(follower, followee.account.inbox, undo); + } + } catch (error) { + done(error); + return; } try { @@ -57,7 +64,7 @@ export default async ({ data }) => { // Publish follow event stream(follower._id, 'unfollow', promisedPackedUser); - } catch (error) { - Logger.error(error.toString()); + } finally { + done(); } }; -- cgit v1.2.3-freya From fba46b4c7f5751cd31463e922edb19f5652ca408 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 5 Apr 2018 01:07:07 +0900 Subject: wip --- src/queue/processors/http/index.ts | 10 +--- src/queue/processors/http/perform-activitypub.ts | 7 --- src/remote/activitypub/resolve-person.ts | 58 ++++++------------------ 3 files changed, 15 insertions(+), 60 deletions(-) delete mode 100644 src/queue/processors/http/perform-activitypub.ts (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts index 8f9aa717c3..06c6b1d1aa 100644 --- a/src/queue/processors/http/index.ts +++ b/src/queue/processors/http/index.ts @@ -1,17 +1,11 @@ -import deliverPost from './deliver-post'; -import follow from './follow'; -import performActivityPub from './perform-activitypub'; +import deliver from './deliver'; import processInbox from './process-inbox'; import reportGitHubFailure from './report-github-failure'; -import unfollow from './unfollow'; const handlers = { - deliverPost, - follow, - performActivityPub, + deliver, processInbox, reportGitHubFailure, - unfollow }; export default (job, done) => handlers[job.data.type](job).then(() => done(), done); diff --git a/src/queue/processors/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts deleted file mode 100644 index 963e532fe5..0000000000 --- a/src/queue/processors/http/perform-activitypub.ts +++ /dev/null @@ -1,7 +0,0 @@ -import User from '../../models/user'; -import act from '../../remote/activitypub/act'; -import Resolver from '../../remote/activitypub/resolver'; - -export default ({ data }) => User.findOne({ _id: data.actor }) - .then(actor => act(new Resolver(), actor, data.outbox)) - .then(Promise.all); diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts index 59be65908e..77d08398be 100644 --- a/src/remote/activitypub/resolve-person.ts +++ b/src/remote/activitypub/resolve-person.ts @@ -1,17 +1,15 @@ 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); -} +import uploadFromUrl from '../../api/drive/upload-from-url'; export default async (value, verifier?: string) => { - const { resolver, object } = await new Resolver().resolveOne(value); + const resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; if ( object === null || @@ -21,24 +19,10 @@ export default async (value, verifier?: string) => { !isValidName(object.name) || !isValidDescription(object.summary) ) { - throw new Error(); + throw new Error('invalid person'); } - const [followers, following, outbox, finger] = await Promise.all([ - resolver.resolveOne(object.followers).then( - resolved => isCollection(resolved.object) ? resolved.object : null, - () => null - ), - 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, verifier), - ]); + const finger = await webFinger(object.id, verifier); const host = toUnicode(finger.subject.replace(/^.*?@/, '')); const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase()); @@ -50,10 +34,10 @@ export default async (value, verifier?: string) => { bannerId: null, createdAt: Date.parse(object.published), description: summaryDOM.textContent, - followersCount: followers ? followers.totalItem || 0 : 0, - followingCount: following ? following.totalItem || 0 : 0, + followersCount: 0, + followingCount: 0, name: object.name, - postsCount: outbox ? outbox.totalItem || 0 : 0, + postsCount: 0, driveCapacity: 1024 * 1024 * 8, // 8MiB username: object.preferredUsername, usernameLower: object.preferredUsername.toLowerCase(), @@ -69,33 +53,17 @@ export default async (value, verifier?: string) => { }, }); - 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) { + ].map(async url => { + if (url === undefined) { return null; } - try { - const created = await create(resolver, user, value); + const img = await uploadFromUrl(url, user); - 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; - } + return img._id; })); User.update({ _id: user._id }, { $set: { avatarId, bannerId } }); -- cgit v1.2.3-freya From a5715ecc1b73d3e3a950c392fa3a466dee810248 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Thu, 5 Apr 2018 01:07:55 +0900 Subject: Do not declare two variables in a statement --- src/queue/processors/http/unfollow.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts index dc50e946c9..d62eb280dc 100644 --- a/src/queue/processors/http/unfollow.ts +++ b/src/queue/processors/http/unfollow.ts @@ -15,7 +15,8 @@ export default async ({ data }, done) => { return; } - let follower, followee; + let follower; + let followee; try { [follower, followee] = await Promise.all([ -- cgit v1.2.3-freya From eb304cb5fb588a3da8742f234cdf05ce6deeaa59 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 5 Apr 2018 01:22:41 +0900 Subject: wip --- src/api/following/delete.ts | 69 +++++++++++++++++++++++++++++ src/queue/processors/http/unfollow.ts | 63 -------------------------- src/remote/activitypub/act/create.ts | 1 - src/remote/activitypub/act/index.ts | 11 +++-- src/remote/activitypub/act/undo.ts | 15 +++++++ src/remote/activitypub/act/undo/index.ts | 27 ----------- src/remote/activitypub/act/undo/unfollow.ts | 11 ----- src/remote/activitypub/act/unfollow.ts | 25 +++++++++++ 8 files changed, 114 insertions(+), 108 deletions(-) create mode 100644 src/api/following/delete.ts delete mode 100644 src/queue/processors/http/unfollow.ts create mode 100644 src/remote/activitypub/act/undo.ts delete mode 100644 src/remote/activitypub/act/undo/index.ts delete mode 100644 src/remote/activitypub/act/undo/unfollow.ts create mode 100644 src/remote/activitypub/act/unfollow.ts (limited to 'src/queue/processors') diff --git a/src/api/following/delete.ts b/src/api/following/delete.ts new file mode 100644 index 0000000000..4cdff7ce1b --- /dev/null +++ b/src/api/following/delete.ts @@ -0,0 +1,69 @@ +import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; +import Following from '../../models/following'; +import FollowingLog from '../../models/following-log'; +import FollowedLog from '../../models/followed-log'; +import event from '../../publishers/stream'; +import context from '../../remote/activitypub/renderer/context'; +import renderFollow from '../../remote/activitypub/renderer/follow'; +import renderUndo from '../../remote/activitypub/renderer/undo'; +import { createHttp } from '../../queue'; + +export default async function(follower: IUser, followee: IUser, activity?) { + const following = await Following.findOne({ + followerId: follower._id, + followeeId: followee._id + }); + + if (following == null) { + console.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); + return; + } + + Following.remove({ + _id: following._id + }); + + //#region Decrement following count + User.update({ _id: follower._id }, { + $inc: { + followingCount: -1 + } + }); + + FollowingLog.insert({ + createdAt: following.createdAt, + userId: follower._id, + count: follower.followingCount - 1 + }); + //#endregion + + //#region Decrement followers count + User.update({ _id: followee._id }, { + $inc: { + followersCount: -1 + } + }); + FollowedLog.insert({ + createdAt: following.createdAt, + userId: followee._id, + count: followee.followersCount - 1 + }); + //#endregion + + // Publish unfollow event + if (isLocalUser(follower)) { + packUser(followee, follower).then(packed => event(follower._id, 'unfollow', packed)); + } + + if (isLocalUser(follower) && isRemoteUser(followee)) { + const content = renderUndo(renderFollow(follower, followee)); + content['@context'] = context; + + createHttp({ + type: 'deliver', + user: follower, + content, + to: followee.account.inbox + }).save(); + } +} diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts deleted file mode 100644 index 801a3612a7..0000000000 --- a/src/queue/processors/http/unfollow.ts +++ /dev/null @@ -1,63 +0,0 @@ -import FollowedLog from '../../../models/followed-log'; -import Following from '../../../models/following'; -import FollowingLog from '../../../models/following-log'; -import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user'; -import stream from '../../../publishers/stream'; -import renderFollow from '../../../remote/activitypub/renderer/follow'; -import renderUndo from '../../../remote/activitypub/renderer/undo'; -import context from '../../../remote/activitypub/renderer/context'; -import request from '../../../remote/request'; -import Logger from '../../../utils/logger'; - -export default async ({ data }) => { - const following = await Following.findOne({ _id: data.id }); - if (following === null) { - return; - } - - const [follower, followee] = await Promise.all([ - User.findOne({ _id: following.followerId }), - User.findOne({ _id: following.followeeId }) - ]); - - if (isLocalUser(follower) && isRemoteUser(followee)) { - const undo = renderUndo(renderFollow(follower, followee)); - undo['@context'] = context; - - await request(follower, followee.account.inbox, undo); - } - - try { - await Promise.all([ - // Delete following - Following.findOneAndDelete({ _id: data.id }), - - // Decrement following count - User.update({ _id: follower._id }, { $inc: { followingCount: -1 } }), - FollowingLog.insert({ - createdAt: new Date(), - userId: follower._id, - count: follower.followingCount - 1 - }), - - // Decrement followers count - User.update({ _id: followee._id }, { $inc: { followersCount: -1 } }), - FollowedLog.insert({ - createdAt: new Date(), - userId: followee._id, - count: followee.followersCount - 1 - }) - ]); - - if (isLocalUser(follower)) { - return; - } - - const promisedPackedUser = packUser(followee, follower); - - // Publish follow event - stream(follower._id, 'unfollow', promisedPackedUser); - } catch (error) { - Logger.error(error.toString()); - } -}; diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts index 957900900f..c486571fc1 100644 --- a/src/remote/activitypub/act/create.ts +++ b/src/remote/activitypub/act/create.ts @@ -1,5 +1,4 @@ import { JSDOM } from 'jsdom'; -const createDOMPurify = require('dompurify'); import Resolver from '../resolver'; import DriveFile from '../../../models/drive-file'; diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts index d78335f16e..f22500acef 100644 --- a/src/remote/activitypub/act/index.ts +++ b/src/remote/activitypub/act/index.ts @@ -2,25 +2,24 @@ import create from './create'; import performDeleteActivity from './delete'; import follow from './follow'; import undo from './undo'; -import Resolver from '../resolver'; import { IObject } from '../type'; -export default async (parentResolver: Resolver, actor, activity: IObject): Promise => { +export default async (actor, activity: IObject): Promise => { switch (activity.type) { case 'Create': - await create(parentResolver, actor, activity); + await create(actor, activity); break; case 'Delete': - await performDeleteActivity(parentResolver, actor, activity); + await performDeleteActivity(actor, activity); break; case 'Follow': - await follow(parentResolver, actor, activity); + await follow(actor, activity); break; case 'Undo': - await undo(parentResolver, actor, activity); + await undo(actor, activity); break; default: diff --git a/src/remote/activitypub/act/undo.ts b/src/remote/activitypub/act/undo.ts new file mode 100644 index 0000000000..b3b83777d1 --- /dev/null +++ b/src/remote/activitypub/act/undo.ts @@ -0,0 +1,15 @@ +import unfollow from './unfollow'; + +export default async (actor, activity): Promise => { + if ('actor' in activity && actor.account.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + switch (activity.object.type) { + case 'Follow': + unfollow(activity.object); + break; + } + + return null; +}; diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts deleted file mode 100644 index aa60d3a4fa..0000000000 --- a/src/remote/activitypub/act/undo/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import act from '../../act'; -import deleteObject from '../../delete'; -import unfollow from './unfollow'; -import Resolver from '../../resolver'; - -export default async (resolver: Resolver, actor, activity): Promise => { - if ('actor' in activity && actor.account.uri !== activity.actor) { - throw new Error(); - } - - const results = await act(resolver, actor, activity.object); - - await Promise.all(results.map(async promisedResult => { - const result = await promisedResult; - - if (result === null || await deleteObject(result) !== null) { - return; - } - - switch (result.object.$ref) { - case 'following': - await unfollow(result.object); - } - })); - - return null; -}; diff --git a/src/remote/activitypub/act/undo/unfollow.ts b/src/remote/activitypub/act/undo/unfollow.ts deleted file mode 100644 index c17e06e8a9..0000000000 --- a/src/remote/activitypub/act/undo/unfollow.ts +++ /dev/null @@ -1,11 +0,0 @@ -import queue from '../../../../queue'; - -export default ({ $id }) => new Promise((resolve, reject) => { - queue.create('http', { type: 'unfollow', id: $id }).save(error => { - if (error) { - reject(error); - } else { - resolve(); - } - }); -}); diff --git a/src/remote/activitypub/act/unfollow.ts b/src/remote/activitypub/act/unfollow.ts new file mode 100644 index 0000000000..e3c9e1c1c8 --- /dev/null +++ b/src/remote/activitypub/act/unfollow.ts @@ -0,0 +1,25 @@ +import parseAcct from '../../../acct/parse'; +import User from '../../../models/user'; +import config from '../../../config'; +import unfollow from '../../../api/following/delete'; + +export default async (actor, activity): Promise => { + const prefix = config.url + '/@'; + const id = activity.object.id || activity.object; + + if (!id.startsWith(prefix)) { + return null; + } + + const { username, host } = parseAcct(id.slice(prefix.length)); + if (host !== null) { + throw new Error(); + } + + const followee = await User.findOne({ username, host }); + if (followee === null) { + throw new Error(); + } + + await unfollow(actor, followee, activity); +}; -- cgit v1.2.3-freya From b6b98752053042b3b37d5b94bc823635edc2061f Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 5 Apr 2018 01:24:01 +0900 Subject: wip --- src/queue/processors/http/process-inbox.ts | 12 ++++++------ src/queue/processors/index.ts | 18 ------------------ 2 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 src/queue/processors/index.ts (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index fff1fbf663..82585d3a6b 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -1,11 +1,11 @@ import * as kue from 'kue'; 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 Resolver from '../../remote/activitypub/resolver'; +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 Resolver from '../../../remote/activitypub/resolver'; // ユーザーのinboxにアクティビティが届いた時の処理 export default async (job: kue.Job, done): Promise => { @@ -47,7 +47,7 @@ export default async (job: kue.Job, done): Promise => { // アクティビティを処理 try { - await act(new Resolver(), user, activity); + await act(user, activity); done(); } catch (e) { done(e); diff --git a/src/queue/processors/index.ts b/src/queue/processors/index.ts deleted file mode 100644 index 172048ddae..0000000000 --- a/src/queue/processors/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import queue from '../queue'; -import db from './db'; -import http from './http'; - -export default () => { - queue.process('db', db); - - /* - 256 is the default concurrency limit of Mozilla Firefox and Google - Chromium. - - a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google - https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff - Network.http.max-connections - MozillaZine Knowledge Base - http://kb.mozillazine.org/Network.http.max-connections - */ - queue.process('http', 256, http); -}; -- cgit v1.2.3-freya From 168b0730b46fd283b900b553dd2eede2aa4c7dac Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Thu, 5 Apr 2018 01:24:39 +0900 Subject: Implement Mention object --- src/post/create.ts | 14 ++------------ src/queue/processors/http/process-inbox.ts | 2 +- src/remote/activitypub/create.ts | 12 +++++++++++- src/remote/activitypub/resolve-person.ts | 5 ++--- src/remote/resolve-user.ts | 3 ++- src/server/api/endpoints/posts/create.ts | 10 +++++++--- 6 files changed, 25 insertions(+), 21 deletions(-) (limited to 'src/queue/processors') diff --git a/src/post/create.ts b/src/post/create.ts index ecea37382d..4ad1503e0f 100644 --- a/src/post/create.ts +++ b/src/post/create.ts @@ -1,8 +1,6 @@ -import parseAcct from '../acct/parse'; import Post from '../models/post'; -import User from '../models/user'; -export default async (post, reply, repost, atMentions) => { +export default async (post, reply, repost, mentions) => { post.mentions = []; function addMention(mentionee) { @@ -36,15 +34,7 @@ export default async (post, reply, repost, atMentions) => { post._repost = null; } - await Promise.all(atMentions.map(async mention => { - // Fetch mentioned user - // SELECT _id - const { _id } = await User - .findOne(parseAcct(mention), { _id: true }); - - // Add mention - addMention(_id); - })); + await Promise.all(mentions.map(({ _id }) => addMention(_id))); return Post.insert(post); }; diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index 88fbb97377..7eeaa19f8a 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -25,7 +25,7 @@ export default async ({ data }, done) => { }) as IRemoteUser; if (user === null) { - user = await resolvePerson(data.signature.keyId); + user = await resolvePerson(new Resolver(), data.signature.keyId); } } diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts index 97c72860fd..710d56fd3d 100644 --- a/src/remote/activitypub/create.ts +++ b/src/remote/activitypub/create.ts @@ -7,6 +7,7 @@ import { IRemoteUser } from '../../models/user'; import uploadFromUrl from '../../drive/upload-from-url'; import createPost from '../../post/create'; import distributePost from '../../post/distribute'; +import resolvePerson from './resolve-person'; import Resolver from './resolver'; const createDOMPurify = require('dompurify'); @@ -53,6 +54,15 @@ class Creator { .map(({ object }) => object.$id); const { window } = new JSDOM(note.content); + const mentions = []; + + for (const { href, type } of note.tags) { + switch (type) { + case 'Mention': + mentions.push(resolvePerson(resolver, href)); + break; + } + } const inserted = await createPost({ channelId: undefined, @@ -69,7 +79,7 @@ class Creator { viaMobile: false, geo: undefined, uri: note.id - }, null, null, []); + }, null, null, await Promise.all(mentions)); const promises = []; diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts index 2cf3ad32d8..7ed01e3222 100644 --- a/src/remote/activitypub/resolve-person.ts +++ b/src/remote/activitypub/resolve-person.ts @@ -4,14 +4,13 @@ import User, { validateUsername, isValidName, isValidDescription } from '../../m import { createHttp } 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, verifier?: string) => { - const { resolver, object } = await new Resolver().resolveOne(value); +export default async (parentResolver, value, verifier?: string) => { + const { resolver, object } = parentResolver.resolveOne(value); if ( object === null || diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts index 48219e8cb3..097ed66738 100644 --- a/src/remote/resolve-user.ts +++ b/src/remote/resolve-user.ts @@ -1,6 +1,7 @@ import { toUnicode, toASCII } from 'punycode'; import User from '../models/user'; import resolvePerson from './activitypub/resolve-person'; +import Resolver from './activitypub/resolver'; import webFinger from './webfinger'; export default async (username, host, option) => { @@ -19,7 +20,7 @@ export default async (username, host, option) => { throw new Error(); } - user = await resolvePerson(self.href, acctLower); + user = await resolvePerson(new Resolver(), self.href, acctLower); } return user; diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts index 03af7ee763..47897626f1 100644 --- a/src/server/api/endpoints/posts/create.ts +++ b/src/server/api/endpoints/posts/create.ts @@ -3,12 +3,13 @@ */ import $ from 'cafy'; import deepEqual = require('deep-equal'); +import parseAcct from '../../../../acct/parse'; import renderAcct from '../../../../acct/render'; import config from '../../../../config'; import html from '../../../../text/html'; import parse from '../../../../text/parse'; import Post, { IPost, isValidText, isValidCw } from '../../../../models/post'; -import { ILocalUser } from '../../../../models/user'; +import User, { ILocalUser } from '../../../../models/user'; import Channel, { IChannel } from '../../../../models/channel'; import DriveFile from '../../../../models/drive-file'; import create from '../../../../post/create'; @@ -267,7 +268,10 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej) .filter(t => t.type == 'mention') .map(renderAcct) // Drop dupulicates - .filter((v, i, s) => s.indexOf(v) == i); + .filter((v, i, s) => s.indexOf(v) == i) + // Fetch mentioned user + // SELECT _id + .map(mention => User.findOne(parseAcct(mention), { _id: true })); } // 投稿を作成 @@ -286,7 +290,7 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej) viaMobile: viaMobile, visibility, geo - }, reply, repost, atMentions); + }, reply, repost, await Promise.all(atMentions)); const postObj = await distribute(user, post.mentions, post); -- cgit v1.2.3-freya From c2c03a1c650c0395fc4d77334ec86d00b1bb24c2 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 5 Apr 2018 03:21:11 +0900 Subject: wip --- src/api/drive/add-file.ts | 12 ++++----- src/api/drive/upload-from-url.ts | 2 +- src/api/post/create.ts | 13 +++++----- src/api/post/watch.ts | 2 +- src/queue/processors/db/delete-post-dependents.ts | 12 ++++----- src/queue/processors/http/process-inbox.ts | 1 - src/remote/activitypub/act/create.ts | 25 ++++++++++++------ src/remote/activitypub/act/delete.ts | 31 ++++++++++++++--------- src/remote/activitypub/act/undo.ts | 2 +- src/remote/activitypub/delete/index.ts | 10 -------- src/remote/activitypub/delete/post.ts | 13 ---------- src/remote/activitypub/resolve-person.ts | 1 - src/server/activitypub/inbox.ts | 4 +-- 13 files changed, 60 insertions(+), 68 deletions(-) delete mode 100644 src/remote/activitypub/delete/index.ts delete mode 100644 src/remote/activitypub/delete/post.ts (limited to 'src/queue/processors') diff --git a/src/api/drive/add-file.ts b/src/api/drive/add-file.ts index 24eb5208d5..64a2f18340 100644 --- a/src/api/drive/add-file.ts +++ b/src/api/drive/add-file.ts @@ -10,12 +10,12 @@ import * as debug from 'debug'; import fileType = require('file-type'); import prominence = require('prominence'); -import DriveFile, { IMetadata, getGridFSBucket } from '../models/drive-file'; -import DriveFolder from '../models/drive-folder'; -import { pack } from '../models/drive-file'; -import event, { publishDriveStream } from '../publishers/stream'; -import getAcct from '../acct/render'; -import config from '../config'; +import DriveFile, { IMetadata, getGridFSBucket } from '../../models/drive-file'; +import DriveFolder from '../../models/drive-folder'; +import { pack } from '../../models/drive-file'; +import event, { publishDriveStream } from '../../publishers/stream'; +import getAcct from '../../acct/render'; +import config from '../../config'; const gm = _gm.subClass({ imageMagick: true diff --git a/src/api/drive/upload-from-url.ts b/src/api/drive/upload-from-url.ts index f96af0f266..26c890d157 100644 --- a/src/api/drive/upload-from-url.ts +++ b/src/api/drive/upload-from-url.ts @@ -1,5 +1,5 @@ import * as URL from 'url'; -import { IDriveFile, validateFileName } from '../models/drive-file'; +import { IDriveFile, validateFileName } from '../../models/drive-file'; import create from './add-file'; import * as debug from 'debug'; import * as tmp from 'tmp'; diff --git a/src/api/post/create.ts b/src/api/post/create.ts index 8256cbc355..36819ec2b8 100644 --- a/src/api/post/create.ts +++ b/src/api/post/create.ts @@ -1,6 +1,5 @@ -import parseAcct from '../../acct/parse'; import Post, { pack, IPost } from '../../models/post'; -import User, { isLocalUser, isRemoteUser, IUser } from '../../models/user'; +import User, { isLocalUser, IUser } from '../../models/user'; import stream from '../../publishers/stream'; import Following from '../../models/following'; import { createHttp } from '../../queue'; @@ -25,14 +24,16 @@ export default async (user: IUser, content: { repost: IPost; media: IDriveFile[]; geo: any; - poll: any; + poll?: any; viaMobile: boolean; - tags: string[]; - cw: string; - visibility: string; + tags?: string[]; + cw?: string; + visibility?: string; uri?: string; app?: IApp; }) => new Promise(async (res, rej) => { + if (content.visibility == null) content.visibility = 'public'; + const tags = content.tags || []; let tokens = null; diff --git a/src/api/post/watch.ts b/src/api/post/watch.ts index 61ea444430..bbd9976f40 100644 --- a/src/api/post/watch.ts +++ b/src/api/post/watch.ts @@ -1,5 +1,5 @@ import * as mongodb from 'mongodb'; -import Watching from '../models/post-watching'; +import Watching from '../../models/post-watching'; export default async (me: mongodb.ObjectID, post: object) => { // 自分の投稿はwatchできない diff --git a/src/queue/processors/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts index 879c41ec9c..6de21eb053 100644 --- a/src/queue/processors/db/delete-post-dependents.ts +++ b/src/queue/processors/db/delete-post-dependents.ts @@ -1,9 +1,9 @@ -import Favorite from '../../models/favorite'; -import Notification from '../../models/notification'; -import PollVote from '../../models/poll-vote'; -import PostReaction from '../../models/post-reaction'; -import PostWatching from '../../models/post-watching'; -import Post from '../../models/post'; +import Favorite from '../../../models/favorite'; +import Notification from '../../../models/notification'; +import PollVote from '../../../models/poll-vote'; +import PostReaction from '../../../models/post-reaction'; +import PostWatching from '../../../models/post-watching'; +import Post from '../../../models/post'; export default async ({ data }) => Promise.all([ Favorite.remove({ postId: data._id }), diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index 82585d3a6b..c3074429f8 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -5,7 +5,6 @@ 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 Resolver from '../../../remote/activitypub/resolver'; // ユーザーのinboxにアクティビティが届いた時の処理 export default async (job: kue.Job, done): Promise => { diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts index c486571fc1..f97832a989 100644 --- a/src/remote/activitypub/act/create.ts +++ b/src/remote/activitypub/act/create.ts @@ -36,17 +36,17 @@ export default async (actor, activity): Promise => { switch (object.type) { case 'Image': - createImage(resolver, object); + createImage(object); break; case 'Note': - createNote(resolver, object); + createNote(object); break; } /// - async function createImage(resolver: Resolver, image) { + async function createImage(image) { if ('attributedTo' in image && actor.account.uri !== image.attributedTo) { throw new Error('invalid image'); } @@ -54,7 +54,7 @@ export default async (actor, activity): Promise => { return await uploadFromUrl(image.url, actor); } - async function createNote(resolver: Resolver, note) { + async function createNote(note) { if ( ('attributedTo' in note && actor.account.uri !== note.attributedTo) || typeof note.id !== 'string' @@ -63,20 +63,29 @@ export default async (actor, activity): Promise => { } const media = []; - if ('attachment' in note) { note.attachment.forEach(async media => { - const created = await createImage(resolver, media); + const created = await createImage(media); media.push(created); }); } + let reply = null; + if ('inReplyTo' in note) { + const inReplyToPost = await Post.findOne({ uri: note.id || note }); + if (inReplyToPost) { + reply = inReplyToPost; + } else { + reply = await createNote(await resolver.resolve(note)); + } + } + const { window } = new JSDOM(note.content); - await createPost(actor, { + return await createPost(actor, { createdAt: new Date(note.published), media, - reply: undefined, + reply, repost: undefined, text: window.document.body.textContent, viaMobile: false, diff --git a/src/remote/activitypub/act/delete.ts b/src/remote/activitypub/act/delete.ts index f9eb4dd08d..334ca47edf 100644 --- a/src/remote/activitypub/act/delete.ts +++ b/src/remote/activitypub/act/delete.ts @@ -1,21 +1,28 @@ -import create from '../create'; -import deleteObject from '../delete'; +import Resolver from '../resolver'; +import Post from '../../../models/post'; +import { createDb } from '../../../queue'; -export default async (resolver, actor, activity) => { +export default async (actor, activity): Promise => { if ('actor' in activity && actor.account.uri !== activity.actor) { throw new Error(); } - const results = await create(resolver, actor, activity.object); + const resolver = new Resolver(); - await Promise.all(results.map(async promisedResult => { - const result = await promisedResult; - if (result === null) { - return; - } + const object = await resolver.resolve(activity); - await deleteObject(result); - })); + switch (object.type) { + case 'Note': + deleteNote(object); + break; + } + + async function deleteNote(note) { + const post = await Post.findOneAndDelete({ uri: note.id }); - return null; + createDb({ + type: 'deletePostDependents', + id: post._id + }).delay(65536).save(); + } }; diff --git a/src/remote/activitypub/act/undo.ts b/src/remote/activitypub/act/undo.ts index b3b83777d1..9d9f6b0359 100644 --- a/src/remote/activitypub/act/undo.ts +++ b/src/remote/activitypub/act/undo.ts @@ -7,7 +7,7 @@ export default async (actor, activity): Promise => { switch (activity.object.type) { case 'Follow': - unfollow(activity.object); + unfollow(actor, activity.object); break; } diff --git a/src/remote/activitypub/delete/index.ts b/src/remote/activitypub/delete/index.ts deleted file mode 100644 index bc9104284b..0000000000 --- a/src/remote/activitypub/delete/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import deletePost from './post'; - -export default async ({ object }) => { - switch (object.$ref) { - case 'posts': - return deletePost(object); - } - - return null; -}; diff --git a/src/remote/activitypub/delete/post.ts b/src/remote/activitypub/delete/post.ts deleted file mode 100644 index f6c816647d..0000000000 --- a/src/remote/activitypub/delete/post.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Post from '../../../models/post'; -import queue from '../../../queue'; - -export default async ({ $id }) => { - const promisedDeletion = Post.findOneAndDelete({ _id: $id }); - - await new Promise((resolve, reject) => queue.create('db', { - type: 'deletePostDependents', - id: $id - }).delay(65536).save(error => error ? reject(error) : resolve())); - - return promisedDeletion; -}; diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts index 77d08398be..28162497f3 100644 --- a/src/remote/activitypub/resolve-person.ts +++ b/src/remote/activitypub/resolve-person.ts @@ -2,7 +2,6 @@ import { JSDOM } from 'jsdom'; import { toUnicode } from 'punycode'; import User, { validateUsername, isValidName, isValidDescription } from '../../models/user'; import webFinger from '../webfinger'; -import create from './create'; import Resolver from './resolver'; import uploadFromUrl from '../../api/drive/upload-from-url'; diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts index 847dc19af6..b0015409a9 100644 --- a/src/server/activitypub/inbox.ts +++ b/src/server/activitypub/inbox.ts @@ -1,7 +1,7 @@ import * as bodyParser from 'body-parser'; import * as express from 'express'; import { parseRequest } from 'http-signature'; -import queue from '../../queue'; +import { createHttp } from '../../queue'; const app = express(); @@ -22,7 +22,7 @@ app.post('/@:user/inbox', bodyParser.json({ return res.sendStatus(401); } - queue.create('http', { + createHttp({ type: 'processInbox', activity: req.body, signature, -- cgit v1.2.3-freya From 7403f38fb43b0ad747236061a591cbf94e198ba6 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 5 Apr 2018 18:43:06 +0900 Subject: wip --- src/api/post/create.ts | 15 ++++++++------- src/index.ts | 2 +- src/queue/index.ts | 8 +++++++- src/queue/processors/http/deliver.ts | 9 ++++++++- src/queue/processors/http/index.ts | 16 ++++++++++++---- src/queue/processors/http/report-github-failure.ts | 6 +++--- src/remote/activitypub/resolver.ts | 2 +- src/remote/request.ts | 8 ++++++++ src/server/api/endpoints/following/create.ts | 11 ++--------- 9 files changed, 50 insertions(+), 27 deletions(-) (limited to 'src/queue/processors') diff --git a/src/api/post/create.ts b/src/api/post/create.ts index 5495117538..dbeb87ae86 100644 --- a/src/api/post/create.ts +++ b/src/api/post/create.ts @@ -18,20 +18,21 @@ import html from '../../text/html'; import { IApp } from '../../models/app'; export default async (user: IUser, content: { - createdAt: Date; - text: string; - reply: IPost; - repost: IPost; - media: IDriveFile[]; - geo: any; + createdAt?: Date; + text?: string; + reply?: IPost; + repost?: IPost; + media?: IDriveFile[]; + geo?: any; poll?: any; - viaMobile: boolean; + viaMobile?: boolean; tags?: string[]; cw?: string; visibility?: string; uri?: string; app?: IApp; }) => new Promise(async (res, rej) => { + if (content.createdAt == null) content.createdAt = new Date(); if (content.visibility == null) content.visibility = 'public'; const tags = content.tags || []; diff --git a/src/index.ts b/src/index.ts index e35c917a44..f45bcaa6ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -103,7 +103,7 @@ async function workerMain(opt) { if (!opt['only-server']) { // start processor - require('./processor').default(); + require('./queue').default(); } // Send a 'ready' message to parent process diff --git a/src/queue/index.ts b/src/queue/index.ts index c8c436b18c..86600dc265 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -1,8 +1,12 @@ import { createQueue } from 'kue'; +import * as debug from 'debug'; + import config from '../config'; import db from './processors/db'; import http from './processors/http'; +const log = debug('misskey:queue'); + const queue = createQueue({ redis: { port: config.redis.port, @@ -12,6 +16,8 @@ const queue = createQueue({ }); export function createHttp(data) { + log(`HTTP job created: ${JSON.stringify(data)}`); + return queue .create('http', data) .attempts(16) @@ -22,7 +28,7 @@ export function createDb(data) { return queue.create('db', data); } -export function process() { +export default function() { queue.process('db', db); /* diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts index 1700063a5d..da7e8bc368 100644 --- a/src/queue/processors/http/deliver.ts +++ b/src/queue/processors/http/deliver.ts @@ -3,5 +3,12 @@ import * as kue from 'kue'; import request from '../../../remote/request'; export default async (job: kue.Job, done): Promise => { - await request(job.data.user, job.data.to, job.data.content); + try { + await request(job.data.user, job.data.to, job.data.content); + done(); + } catch (e) { + console.warn(`deliver failed: ${e}`); + + done(e); + } }; diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts index 06c6b1d1aa..3d7d941b1a 100644 --- a/src/queue/processors/http/index.ts +++ b/src/queue/processors/http/index.ts @@ -3,9 +3,17 @@ import processInbox from './process-inbox'; import reportGitHubFailure from './report-github-failure'; const handlers = { - deliver, - processInbox, - reportGitHubFailure, + deliver, + processInbox, + reportGitHubFailure }; -export default (job, done) => handlers[job.data.type](job).then(() => done(), done); +export default (job, done) => { + const handler = handlers[job.data.type]; + + if (handler) { + handler(job).then(() => done(), done); + } else { + console.warn(`Unknown job: ${job.data.type}`); + } +}; diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts index 4f6f5ccee5..e747d062d3 100644 --- a/src/queue/processors/http/report-github-failure.ts +++ b/src/queue/processors/http/report-github-failure.ts @@ -1,6 +1,6 @@ import * as request from 'request-promise-native'; -import User from '../../models/user'; -const createPost = require('../../server/api/endpoints/posts/create'); +import User from '../../../models/user'; +import createPost from '../../../api/post/create'; export default async ({ data }) => { const asyncBot = User.findOne({ _id: data.userId }); @@ -20,5 +20,5 @@ export default async ({ data }) => { `**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` : `**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`; - createPost({ text }, await asyncBot); + createPost(await asyncBot, { text }); }; diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 09a6e70056..38639c6813 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -59,7 +59,7 @@ export default class Resolver { throw new Error('invalid response'); } - log(`resolved: ${JSON.stringify(object)}`); + log(`resolved: ${JSON.stringify(object, null, 2)}`); return object; } diff --git a/src/remote/request.ts b/src/remote/request.ts index 72262cbf61..a375aebfbb 100644 --- a/src/remote/request.ts +++ b/src/remote/request.ts @@ -1,9 +1,15 @@ import { request } from 'https'; import { sign } from 'http-signature'; import { URL } from 'url'; +import * as debug from 'debug'; + import config from '../config'; +const log = debug('misskey:activitypub:deliver'); + export default ({ account, username }, url, object) => new Promise((resolve, reject) => { + log(`--> ${url}`); + const { protocol, hostname, port, pathname, search } = new URL(url); const req = request({ @@ -14,6 +20,8 @@ export default ({ account, username }, url, object) => new Promise((resolve, rej path: pathname + search, }, res => { res.on('end', () => { + log(`${url} --> ${res.statusCode}`); + if (res.statusCode >= 200 && res.statusCode < 300) { resolve(); } else { diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index e568595215..fae686ce54 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import User from '../../../../models/user'; import Following from '../../../../models/following'; -import queue from '../../../../queue'; +import create from '../../../../api/following/create'; /** * Follow a user @@ -50,15 +50,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Create following - const { _id } = await Following.insert({ - createdAt: new Date(), - followerId: follower._id, - followeeId: followee._id - }); - - queue.create('http', { type: 'follow', following: _id }).save(); + create(follower, followee); // Send response res(); - }); -- cgit v1.2.3-freya From b6aeacdeb942beb7b5b2f6ac8cf4a89163e59153 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 6 Apr 2018 03:10:25 +0900 Subject: RENAME: api --> services --- src/api/drive/add-file.ts | 314 ------------------- src/api/drive/upload-from-url.ts | 52 ---- src/api/following/create.ts | 72 ----- src/api/following/delete.ts | 64 ---- src/api/post/create.ts | 344 --------------------- src/api/post/watch.ts | 26 -- src/queue/processors/http/report-github-failure.ts | 2 +- src/remote/activitypub/act/create.ts | 4 +- src/remote/activitypub/act/follow.ts | 2 +- src/remote/activitypub/act/unfollow.ts | 2 +- src/remote/activitypub/resolve-person.ts | 2 +- src/server/api/endpoints/following/create.ts | 2 +- src/server/api/endpoints/posts/create.ts | 2 +- src/services/drive/add-file.ts | 314 +++++++++++++++++++ src/services/drive/upload-from-url.ts | 52 ++++ src/services/following/create.ts | 72 +++++ src/services/following/delete.ts | 64 ++++ src/services/post/create.ts | 344 +++++++++++++++++++++ src/services/post/reaction/create.ts | 0 src/services/post/watch.ts | 26 ++ 20 files changed, 880 insertions(+), 880 deletions(-) delete mode 100644 src/api/drive/add-file.ts delete mode 100644 src/api/drive/upload-from-url.ts delete mode 100644 src/api/following/create.ts delete mode 100644 src/api/following/delete.ts delete mode 100644 src/api/post/create.ts delete mode 100644 src/api/post/watch.ts create mode 100644 src/services/drive/add-file.ts create mode 100644 src/services/drive/upload-from-url.ts create mode 100644 src/services/following/create.ts create mode 100644 src/services/following/delete.ts create mode 100644 src/services/post/create.ts create mode 100644 src/services/post/reaction/create.ts create mode 100644 src/services/post/watch.ts (limited to 'src/queue/processors') diff --git a/src/api/drive/add-file.ts b/src/api/drive/add-file.ts deleted file mode 100644 index 64a2f18340..0000000000 --- a/src/api/drive/add-file.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { Buffer } from 'buffer'; -import * as fs from 'fs'; -import * as tmp from 'tmp'; -import * as stream from 'stream'; - -import * as mongodb from 'mongodb'; -import * as crypto from 'crypto'; -import * as _gm from 'gm'; -import * as debug from 'debug'; -import fileType = require('file-type'); -import prominence = require('prominence'); - -import DriveFile, { IMetadata, getGridFSBucket } from '../../models/drive-file'; -import DriveFolder from '../../models/drive-folder'; -import { pack } from '../../models/drive-file'; -import event, { publishDriveStream } from '../../publishers/stream'; -import getAcct from '../../acct/render'; -import config from '../../config'; - -const gm = _gm.subClass({ - imageMagick: true -}); - -const log = debug('misskey:drive:add-file'); - -const tmpFile = (): Promise => new Promise((resolve, reject) => { - tmp.file((e, path) => { - if (e) return reject(e); - resolve(path); - }); -}); - -const addToGridFS = (name: string, readable: stream.Readable, type: string, metadata: any): Promise => - getGridFSBucket() - .then(bucket => new Promise((resolve, reject) => { - const writeStream = bucket.openUploadStream(name, { contentType: type, metadata }); - writeStream.once('finish', (doc) => { resolve(doc); }); - writeStream.on('error', reject); - readable.pipe(writeStream); - })); - -const addFile = async ( - user: any, - path: string, - name: string = null, - comment: string = null, - folderId: mongodb.ObjectID = null, - force: boolean = false, - uri: string = null -) => { - log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`); - - // Calculate hash, get content type and get file size - const [hash, [mime, ext], size] = await Promise.all([ - // hash - ((): Promise => new Promise((res, rej) => { - const readable = fs.createReadStream(path); - const hash = crypto.createHash('md5'); - const chunks = []; - readable - .on('error', rej) - .pipe(hash) - .on('error', rej) - .on('data', (chunk) => chunks.push(chunk)) - .on('end', () => { - const buffer = Buffer.concat(chunks); - res(buffer.toString('hex')); - }); - }))(), - // mime - ((): Promise<[string, string | null]> => new Promise((res, rej) => { - const readable = fs.createReadStream(path); - readable - .on('error', rej) - .once('data', (buffer: Buffer) => { - readable.destroy(); - const type = fileType(buffer); - if (type) { - return res([type.mime, type.ext]); - } else { - // 種類が同定できなかったら application/octet-stream にする - return res(['application/octet-stream', null]); - } - }); - }))(), - // size - ((): Promise => new Promise((res, rej) => { - fs.stat(path, (err, stats) => { - if (err) return rej(err); - res(stats.size); - }); - }))() - ]); - - log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`); - - // detect name - const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled'); - - if (!force) { - // Check if there is a file with the same hash - const much = await DriveFile.findOne({ - md5: hash, - 'metadata.userId': user._id - }); - - if (much !== null) { - log('file with same hash is found'); - return much; - } else { - log('file with same hash is not found'); - } - } - - const [wh, averageColor, folder] = await Promise.all([ - // Width and height (when image) - (async () => { - // 画像かどうか - if (!/^image\/.*$/.test(mime)) { - return null; - } - - const imageType = mime.split('/')[1]; - - // 画像でもPNGかJPEGかGIFでないならスキップ - if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') { - return null; - } - - log('calculate image width and height...'); - - // Calculate width and height - const g = gm(fs.createReadStream(path), name); - const size = await prominence(g).size(); - - log(`image width and height is calculated: ${size.width}, ${size.height}`); - - return [size.width, size.height]; - })(), - // average color (when image) - (async () => { - // 画像かどうか - if (!/^image\/.*$/.test(mime)) { - return null; - } - - const imageType = mime.split('/')[1]; - - // 画像でもPNGかJPEGでないならスキップ - if (imageType != 'png' && imageType != 'jpeg') { - return null; - } - - log('calculate average color...'); - - const buffer = await prominence(gm(fs.createReadStream(path), name) - .setFormat('ppm') - .resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック - .toBuffer(); - - const r = buffer.readUInt8(buffer.length - 3); - const g = buffer.readUInt8(buffer.length - 2); - const b = buffer.readUInt8(buffer.length - 1); - - log(`average color is calculated: ${r}, ${g}, ${b}`); - - return [r, g, b]; - })(), - // folder - (async () => { - if (!folderId) { - return null; - } - const driveFolder = await DriveFolder.findOne({ - _id: folderId, - userId: user._id - }); - if (!driveFolder) { - throw 'folder-not-found'; - } - return driveFolder; - })(), - // usage checker - (async () => { - // Calculate drive usage - const usage = await DriveFile - .aggregate([{ - $match: { 'metadata.userId': user._id } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then((aggregates: any[]) => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - log(`drive usage is ${usage}`); - - // If usage limit exceeded - if (usage + size > user.driveCapacity) { - throw 'no-free-space'; - } - })() - ]); - - const readable = fs.createReadStream(path); - - const properties = {}; - - if (wh) { - properties['width'] = wh[0]; - properties['height'] = wh[1]; - } - - if (averageColor) { - properties['avgColor'] = averageColor; - } - - const metadata = { - userId: user._id, - folderId: folder !== null ? folder._id : null, - comment: comment, - properties: properties - } as IMetadata; - - if (uri !== null) { - metadata.uri = uri; - } - - return addToGridFS(detectedName, readable, mime, metadata); -}; - -/** - * Add file to drive - * - * @param user User who wish to add file - * @param file File path or readableStream - * @param comment Comment - * @param type File type - * @param folderId Folder ID - * @param force If set to true, forcibly upload the file even if there is a file with the same hash. - * @return Object that represents added file - */ -export default (user: any, file: string | stream.Readable, ...args) => new Promise((resolve, reject) => { - // Get file path - new Promise((res: (v: [string, boolean]) => void, rej) => { - if (typeof file === 'string') { - res([file, false]); - return; - } - if (typeof file === 'object' && typeof file.read === 'function') { - tmpFile() - .then(path => { - const readable: stream.Readable = file; - const writable = fs.createWriteStream(path); - readable - .on('error', rej) - .on('end', () => { - res([path, true]); - }) - .pipe(writable) - .on('error', rej); - }) - .catch(rej); - } - rej(new Error('un-compatible file.')); - }) - .then(([path, shouldCleanup]): Promise => new Promise((res, rej) => { - addFile(user, path, ...args) - .then(file => { - res(file); - if (shouldCleanup) { - fs.unlink(path, (e) => { - if (e) log(e.stack); - }); - } - }) - .catch(rej); - })) - .then(file => { - log(`drive file has been created ${file._id}`); - resolve(file); - - pack(file).then(serializedFile => { - // Publish drive_file_created event - event(user._id, 'drive_file_created', serializedFile); - publishDriveStream(user._id, 'file_created', serializedFile); - - // Register to search database - if (config.elasticsearch.enable) { - const es = require('../db/elasticsearch'); - es.index({ - index: 'misskey', - type: 'drive_file', - id: file._id.toString(), - body: { - name: file.name, - userId: user._id.toString() - } - }); - } - }); - }) - .catch(reject); -}); diff --git a/src/api/drive/upload-from-url.ts b/src/api/drive/upload-from-url.ts deleted file mode 100644 index 676586cd15..0000000000 --- a/src/api/drive/upload-from-url.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as URL from 'url'; -import { IDriveFile, validateFileName } from '../../models/drive-file'; -import create from './add-file'; -import * as debug from 'debug'; -import * as tmp from 'tmp'; -import * as fs from 'fs'; -import * as request from 'request'; - -const log = debug('misskey:drive:upload-from-url'); - -export default async (url, user, folderId = null, uri = null): Promise => { - log(`REQUESTED: ${url}`); - - let name = URL.parse(url).pathname.split('/').pop(); - if (!validateFileName(name)) { - name = null; - } - - log(`name: ${name}`); - - // Create temp file - const path = await new Promise((res: (string) => void, rej) => { - tmp.file((e, path) => { - if (e) return rej(e); - res(path); - }); - }); - - // write content at URL to temp file - await new Promise((res, rej) => { - const writable = fs.createWriteStream(path); - request(url) - .on('error', rej) - .on('end', () => { - writable.close(); - res(path); - }) - .pipe(writable) - .on('error', rej); - }); - - const driveFile = await create(user, path, name, null, folderId, false, uri); - - log(`created: ${driveFile._id}`); - - // clean-up - fs.unlink(path, (e) => { - if (e) log(e.stack); - }); - - return driveFile; -}; diff --git a/src/api/following/create.ts b/src/api/following/create.ts deleted file mode 100644 index d919f4487f..0000000000 --- a/src/api/following/create.ts +++ /dev/null @@ -1,72 +0,0 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; -import FollowingLog from '../../models/following-log'; -import FollowedLog from '../../models/followed-log'; -import event from '../../publishers/stream'; -import notify from '../../publishers/notify'; -import context from '../../remote/activitypub/renderer/context'; -import renderFollow from '../../remote/activitypub/renderer/follow'; -import renderAccept from '../../remote/activitypub/renderer/accept'; -import { deliver } from '../../queue'; - -export default async function(follower: IUser, followee: IUser, activity?) { - const following = await Following.insert({ - createdAt: new Date(), - followerId: follower._id, - followeeId: followee._id - }); - - //#region Increment following count - User.update({ _id: follower._id }, { - $inc: { - followingCount: 1 - } - }); - - FollowingLog.insert({ - createdAt: following.createdAt, - userId: follower._id, - count: follower.followingCount + 1 - }); - //#endregion - - //#region Increment followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: 1 - } - }); - FollowedLog.insert({ - createdAt: following.createdAt, - userId: followee._id, - count: followee.followersCount + 1 - }); - //#endregion - - // Publish follow event - if (isLocalUser(follower)) { - packUser(followee, follower).then(packed => event(follower._id, 'follow', packed)); - } - - // Publish followed event - if (isLocalUser(followee)) { - packUser(follower, followee).then(packed => event(followee._id, 'followed', packed)), - - // 通知を作成 - notify(followee._id, follower._id, 'follow'); - } - - if (isLocalUser(follower) && isRemoteUser(followee)) { - const content = renderFollow(follower, followee); - content['@context'] = context; - - deliver(follower, content, followee.account.inbox).save(); - } - - if (isRemoteUser(follower) && isLocalUser(followee)) { - const content = renderAccept(activity); - content['@context'] = context; - - deliver(followee, content, follower.account.inbox).save(); - } -} diff --git a/src/api/following/delete.ts b/src/api/following/delete.ts deleted file mode 100644 index 364a4803b9..0000000000 --- a/src/api/following/delete.ts +++ /dev/null @@ -1,64 +0,0 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; -import FollowingLog from '../../models/following-log'; -import FollowedLog from '../../models/followed-log'; -import event from '../../publishers/stream'; -import context from '../../remote/activitypub/renderer/context'; -import renderFollow from '../../remote/activitypub/renderer/follow'; -import renderUndo from '../../remote/activitypub/renderer/undo'; -import { deliver } from '../../queue'; - -export default async function(follower: IUser, followee: IUser, activity?) { - const following = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id - }); - - if (following == null) { - console.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); - return; - } - - Following.remove({ - _id: following._id - }); - - //#region Decrement following count - User.update({ _id: follower._id }, { - $inc: { - followingCount: -1 - } - }); - - FollowingLog.insert({ - createdAt: following.createdAt, - userId: follower._id, - count: follower.followingCount - 1 - }); - //#endregion - - //#region Decrement followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: -1 - } - }); - FollowedLog.insert({ - createdAt: following.createdAt, - userId: followee._id, - count: followee.followersCount - 1 - }); - //#endregion - - // Publish unfollow event - if (isLocalUser(follower)) { - packUser(followee, follower).then(packed => event(follower._id, 'unfollow', packed)); - } - - if (isLocalUser(follower) && isRemoteUser(followee)) { - const content = renderUndo(renderFollow(follower, followee)); - content['@context'] = context; - - deliver(follower, content, followee.account.inbox).save(); - } -} diff --git a/src/api/post/create.ts b/src/api/post/create.ts deleted file mode 100644 index 9723dbe452..0000000000 --- a/src/api/post/create.ts +++ /dev/null @@ -1,344 +0,0 @@ -import Post, { pack, IPost } from '../../models/post'; -import User, { isLocalUser, IUser } from '../../models/user'; -import stream from '../../publishers/stream'; -import Following from '../../models/following'; -import { deliver } from '../../queue'; -import renderNote from '../../remote/activitypub/renderer/note'; -import renderCreate from '../../remote/activitypub/renderer/create'; -import context from '../../remote/activitypub/renderer/context'; -import { IDriveFile } from '../../models/drive-file'; -import notify from '../../publishers/notify'; -import PostWatching from '../../models/post-watching'; -import watch from './watch'; -import Mute from '../../models/mute'; -import pushSw from '../../publishers/push-sw'; -import event from '../../publishers/stream'; -import parse from '../../text/parse'; -import html from '../../text/html'; -import { IApp } from '../../models/app'; - -export default async (user: IUser, content: { - createdAt?: Date; - text?: string; - reply?: IPost; - repost?: IPost; - media?: IDriveFile[]; - geo?: any; - poll?: any; - viaMobile?: boolean; - tags?: string[]; - cw?: string; - visibility?: string; - uri?: string; - app?: IApp; -}, silent = false) => new Promise(async (res, rej) => { - if (content.createdAt == null) content.createdAt = new Date(); - if (content.visibility == null) content.visibility = 'public'; - - const tags = content.tags || []; - - let tokens = null; - - if (content.text) { - // Analyze - tokens = parse(content.text); - - // Extract hashtags - const hashtags = tokens - .filter(t => t.type == 'hashtag') - .map(t => t.hashtag); - - hashtags.forEach(tag => { - if (tags.indexOf(tag) == -1) { - tags.push(tag); - } - }); - } - - const data: any = { - createdAt: content.createdAt, - mediaIds: content.media ? content.media.map(file => file._id) : [], - replyId: content.reply ? content.reply._id : null, - repostId: content.repost ? content.repost._id : null, - text: content.text, - textHtml: tokens === null ? null : html(tokens), - poll: content.poll, - cw: content.cw, - tags, - userId: user._id, - viaMobile: content.viaMobile, - geo: content.geo || null, - appId: content.app ? content.app._id : null, - visibility: content.visibility, - - // 以下非正規化データ - _reply: content.reply ? { userId: content.reply.userId } : null, - _repost: content.repost ? { userId: content.repost.userId } : null, - }; - - if (content.uri != null) data.uri = content.uri; - - // 投稿を作成 - const post = await Post.insert(data); - - res(post); - - User.update({ _id: user._id }, { - // Increment posts count - $inc: { - postsCount: 1 - }, - // Update latest post - $set: { - latestPost: post - } - }); - - // Serialize - const postObj = await pack(post); - - // タイムラインへの投稿 - if (!post.channelId) { - // Publish event to myself's stream - if (isLocalUser(user)) { - stream(post.userId, 'post', postObj); - } - - // Fetch all followers - const followers = await Following.aggregate([{ - $lookup: { - from: 'users', - localField: 'followerId', - foreignField: '_id', - as: 'follower' - } - }, { - $match: { - followeeId: post.userId - } - }], { - _id: false - }); - - if (!silent) { - const note = await renderNote(user, post); - const content = renderCreate(note); - content['@context'] = context; - - Promise.all(followers.map(({ follower }) => { - if (isLocalUser(follower)) { - // Publish event to followers stream - stream(follower._id, 'post', postObj); - } else { - // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 - if (isLocalUser(user)) { - deliver(user, content, follower.account.inbox).save(); - } - } - })); - } - } - - // チャンネルへの投稿 - /* TODO - if (post.channelId) { - promises.push( - // Increment channel index(posts count) - Channel.update({ _id: post.channelId }, { - $inc: { - index: 1 - } - }), - - // Publish event to channel - promisedPostObj.then(postObj => { - publishChannelStream(post.channelId, 'post', postObj); - }), - - Promise.all([ - promisedPostObj, - - // Get channel watchers - ChannelWatching.find({ - channelId: post.channelId, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }) - ]).then(([postObj, watches]) => { - // チャンネルの視聴者(のタイムライン)に配信 - watches.forEach(w => { - stream(w.userId, 'post', postObj); - }); - }) - ); - }*/ - - const mentions = []; - - async function addMention(mentionee, reason) { - // Reject if already added - if (mentions.some(x => x.equals(mentionee))) return; - - // Add mention - mentions.push(mentionee); - - // Publish event - if (!user._id.equals(mentionee)) { - const mentioneeMutes = await Mute.find({ - muter_id: mentionee, - deleted_at: { $exists: false } - }); - const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); - if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) { - event(mentionee, reason, postObj); - pushSw(mentionee, reason, postObj); - } - } - } - - // If has in reply to post - if (content.reply) { - // Increment replies count - Post.update({ _id: content.reply._id }, { - $inc: { - repliesCount: 1 - } - }); - - // (自分自身へのリプライでない限りは)通知を作成 - notify(content.reply.userId, user._id, 'reply', { - postId: post._id - }); - - // Fetch watchers - PostWatching.find({ - postId: content.reply._id, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }).then(watchers => { - watchers.forEach(watcher => { - notify(watcher.userId, user._id, 'reply', { - postId: post._id - }); - }); - }); - - // この投稿をWatchする - if (isLocalUser(user) && user.account.settings.autoWatch !== false) { - watch(user._id, content.reply); - } - - // Add mention - addMention(content.reply.userId, 'reply'); - } - - // If it is repost - if (content.repost) { - // Notify - const type = content.text ? 'quote' : 'repost'; - notify(content.repost.userId, user._id, type, { - post_id: post._id - }); - - // Fetch watchers - PostWatching.find({ - postId: content.repost._id, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }).then(watchers => { - watchers.forEach(watcher => { - notify(watcher.userId, user._id, type, { - postId: post._id - }); - }); - }); - - // この投稿をWatchする - if (isLocalUser(user) && user.account.settings.autoWatch !== false) { - watch(user._id, content.repost); - } - - // If it is quote repost - if (content.text) { - // Add mention - addMention(content.repost.userId, 'quote'); - } else { - // Publish event - if (!user._id.equals(content.repost.userId)) { - event(content.repost.userId, 'repost', postObj); - } - } - - // 今までで同じ投稿をRepostしているか - const existRepost = await Post.findOne({ - userId: user._id, - repostId: content.repost._id, - _id: { - $ne: post._id - } - }); - - if (!existRepost) { - // Update repostee status - Post.update({ _id: content.repost._id }, { - $inc: { - repostCount: 1 - } - }); - } - } - - // If has text content - if (content.text) { - // Extract an '@' mentions - const atMentions = tokens - .filter(t => t.type == 'mention') - .map(m => m.username) - // Drop dupulicates - .filter((v, i, s) => s.indexOf(v) == i); - - // Resolve all mentions - await Promise.all(atMentions.map(async mention => { - // Fetch mentioned user - // SELECT _id - const mentionee = await User - .findOne({ - usernameLower: mention.toLowerCase() - }, { _id: true }); - - // When mentioned user not found - if (mentionee == null) return; - - // 既に言及されたユーザーに対する返信や引用repostの場合も無視 - if (content.reply && content.reply.userId.equals(mentionee._id)) return; - if (content.repost && content.repost.userId.equals(mentionee._id)) return; - - // Add mention - addMention(mentionee._id, 'mention'); - - // Create notification - notify(mentionee._id, user._id, 'mention', { - post_id: post._id - }); - })); - } - - // Append mentions data - if (mentions.length > 0) { - Post.update({ _id: post._id }, { - $set: { - mentions - } - }); - } -}); diff --git a/src/api/post/watch.ts b/src/api/post/watch.ts deleted file mode 100644 index bbd9976f40..0000000000 --- a/src/api/post/watch.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as mongodb from 'mongodb'; -import Watching from '../../models/post-watching'; - -export default async (me: mongodb.ObjectID, post: object) => { - // 自分の投稿はwatchできない - if (me.equals((post as any).userId)) { - return; - } - - // if watching now - const exist = await Watching.findOne({ - postId: (post as any)._id, - userId: me, - deletedAt: { $exists: false } - }); - - if (exist !== null) { - return; - } - - await Watching.insert({ - createdAt: new Date(), - postId: (post as any)._id, - userId: me - }); -}; diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts index e747d062d3..1e0b51f89f 100644 --- a/src/queue/processors/http/report-github-failure.ts +++ b/src/queue/processors/http/report-github-failure.ts @@ -1,6 +1,6 @@ import * as request from 'request-promise-native'; import User from '../../../models/user'; -import createPost from '../../../api/post/create'; +import createPost from '../../../services/post/create'; export default async ({ data }) => { const asyncBot = User.findOne({ _id: data.userId }); diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts index fe58f58f87..139c98f3b3 100644 --- a/src/remote/activitypub/act/create.ts +++ b/src/remote/activitypub/act/create.ts @@ -3,8 +3,8 @@ import * as debug from 'debug'; import Resolver from '../resolver'; import Post from '../../../models/post'; -import uploadFromUrl from '../../../api/drive/upload-from-url'; -import createPost from '../../../api/post/create'; +import uploadFromUrl from '../../../services/drive/upload-from-url'; +import createPost from '../../../services/post/create'; import { IRemoteUser, isRemoteUser } from '../../../models/user'; import resolvePerson from '../resolve-person'; diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts index dc173a0acb..4fc423d15c 100644 --- a/src/remote/activitypub/act/follow.ts +++ b/src/remote/activitypub/act/follow.ts @@ -1,7 +1,7 @@ import parseAcct from '../../../acct/parse'; import User from '../../../models/user'; import config from '../../../config'; -import follow from '../../../api/following/create'; +import follow from '../../../services/following/create'; export default async (actor, activity): Promise => { const prefix = config.url + '/@'; diff --git a/src/remote/activitypub/act/unfollow.ts b/src/remote/activitypub/act/unfollow.ts index e3c9e1c1c8..66c15e9a91 100644 --- a/src/remote/activitypub/act/unfollow.ts +++ b/src/remote/activitypub/act/unfollow.ts @@ -1,7 +1,7 @@ import parseAcct from '../../../acct/parse'; import User from '../../../models/user'; import config from '../../../config'; -import unfollow from '../../../api/following/delete'; +import unfollow from '../../../services/following/delete'; export default async (actor, activity): Promise => { const prefix = config.url + '/@'; diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts index 2bf7a13540..907f198342 100644 --- a/src/remote/activitypub/resolve-person.ts +++ b/src/remote/activitypub/resolve-person.ts @@ -3,7 +3,7 @@ import { toUnicode } from 'punycode'; import User, { validateUsername, isValidName, isValidDescription } from '../../models/user'; import webFinger from '../webfinger'; import Resolver from './resolver'; -import uploadFromUrl from '../../api/drive/upload-from-url'; +import uploadFromUrl from '../../services/drive/upload-from-url'; export default async (value, verifier?: string) => { const resolver = new Resolver(); diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index fae686ce54..0ccac8d83d 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import User from '../../../../models/user'; import Following from '../../../../models/following'; -import create from '../../../../api/following/create'; +import create from '../../../../services/following/create'; /** * Follow a user diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts index d241c8c387..003a892bc0 100644 --- a/src/server/api/endpoints/posts/create.ts +++ b/src/server/api/endpoints/posts/create.ts @@ -7,7 +7,7 @@ import Post, { IPost, isValidText, isValidCw, pack } from '../../../../models/po import { ILocalUser } from '../../../../models/user'; import Channel, { IChannel } from '../../../../models/channel'; import DriveFile from '../../../../models/drive-file'; -import create from '../../../../api/post/create'; +import create from '../../../../services/post/create'; import { IApp } from '../../../../models/app'; /** diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts new file mode 100644 index 0000000000..64a2f18340 --- /dev/null +++ b/src/services/drive/add-file.ts @@ -0,0 +1,314 @@ +import { Buffer } from 'buffer'; +import * as fs from 'fs'; +import * as tmp from 'tmp'; +import * as stream from 'stream'; + +import * as mongodb from 'mongodb'; +import * as crypto from 'crypto'; +import * as _gm from 'gm'; +import * as debug from 'debug'; +import fileType = require('file-type'); +import prominence = require('prominence'); + +import DriveFile, { IMetadata, getGridFSBucket } from '../../models/drive-file'; +import DriveFolder from '../../models/drive-folder'; +import { pack } from '../../models/drive-file'; +import event, { publishDriveStream } from '../../publishers/stream'; +import getAcct from '../../acct/render'; +import config from '../../config'; + +const gm = _gm.subClass({ + imageMagick: true +}); + +const log = debug('misskey:drive:add-file'); + +const tmpFile = (): Promise => new Promise((resolve, reject) => { + tmp.file((e, path) => { + if (e) return reject(e); + resolve(path); + }); +}); + +const addToGridFS = (name: string, readable: stream.Readable, type: string, metadata: any): Promise => + getGridFSBucket() + .then(bucket => new Promise((resolve, reject) => { + const writeStream = bucket.openUploadStream(name, { contentType: type, metadata }); + writeStream.once('finish', (doc) => { resolve(doc); }); + writeStream.on('error', reject); + readable.pipe(writeStream); + })); + +const addFile = async ( + user: any, + path: string, + name: string = null, + comment: string = null, + folderId: mongodb.ObjectID = null, + force: boolean = false, + uri: string = null +) => { + log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`); + + // Calculate hash, get content type and get file size + const [hash, [mime, ext], size] = await Promise.all([ + // hash + ((): Promise => new Promise((res, rej) => { + const readable = fs.createReadStream(path); + const hash = crypto.createHash('md5'); + const chunks = []; + readable + .on('error', rej) + .pipe(hash) + .on('error', rej) + .on('data', (chunk) => chunks.push(chunk)) + .on('end', () => { + const buffer = Buffer.concat(chunks); + res(buffer.toString('hex')); + }); + }))(), + // mime + ((): Promise<[string, string | null]> => new Promise((res, rej) => { + const readable = fs.createReadStream(path); + readable + .on('error', rej) + .once('data', (buffer: Buffer) => { + readable.destroy(); + const type = fileType(buffer); + if (type) { + return res([type.mime, type.ext]); + } else { + // 種類が同定できなかったら application/octet-stream にする + return res(['application/octet-stream', null]); + } + }); + }))(), + // size + ((): Promise => new Promise((res, rej) => { + fs.stat(path, (err, stats) => { + if (err) return rej(err); + res(stats.size); + }); + }))() + ]); + + log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`); + + // detect name + const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled'); + + if (!force) { + // Check if there is a file with the same hash + const much = await DriveFile.findOne({ + md5: hash, + 'metadata.userId': user._id + }); + + if (much !== null) { + log('file with same hash is found'); + return much; + } else { + log('file with same hash is not found'); + } + } + + const [wh, averageColor, folder] = await Promise.all([ + // Width and height (when image) + (async () => { + // 画像かどうか + if (!/^image\/.*$/.test(mime)) { + return null; + } + + const imageType = mime.split('/')[1]; + + // 画像でもPNGかJPEGかGIFでないならスキップ + if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') { + return null; + } + + log('calculate image width and height...'); + + // Calculate width and height + const g = gm(fs.createReadStream(path), name); + const size = await prominence(g).size(); + + log(`image width and height is calculated: ${size.width}, ${size.height}`); + + return [size.width, size.height]; + })(), + // average color (when image) + (async () => { + // 画像かどうか + if (!/^image\/.*$/.test(mime)) { + return null; + } + + const imageType = mime.split('/')[1]; + + // 画像でもPNGかJPEGでないならスキップ + if (imageType != 'png' && imageType != 'jpeg') { + return null; + } + + log('calculate average color...'); + + const buffer = await prominence(gm(fs.createReadStream(path), name) + .setFormat('ppm') + .resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック + .toBuffer(); + + const r = buffer.readUInt8(buffer.length - 3); + const g = buffer.readUInt8(buffer.length - 2); + const b = buffer.readUInt8(buffer.length - 1); + + log(`average color is calculated: ${r}, ${g}, ${b}`); + + return [r, g, b]; + })(), + // folder + (async () => { + if (!folderId) { + return null; + } + const driveFolder = await DriveFolder.findOne({ + _id: folderId, + userId: user._id + }); + if (!driveFolder) { + throw 'folder-not-found'; + } + return driveFolder; + })(), + // usage checker + (async () => { + // Calculate drive usage + const usage = await DriveFile + .aggregate([{ + $match: { 'metadata.userId': user._id } + }, { + $project: { + length: true + } + }, { + $group: { + _id: null, + usage: { $sum: '$length' } + } + }]) + .then((aggregates: any[]) => { + if (aggregates.length > 0) { + return aggregates[0].usage; + } + return 0; + }); + + log(`drive usage is ${usage}`); + + // If usage limit exceeded + if (usage + size > user.driveCapacity) { + throw 'no-free-space'; + } + })() + ]); + + const readable = fs.createReadStream(path); + + const properties = {}; + + if (wh) { + properties['width'] = wh[0]; + properties['height'] = wh[1]; + } + + if (averageColor) { + properties['avgColor'] = averageColor; + } + + const metadata = { + userId: user._id, + folderId: folder !== null ? folder._id : null, + comment: comment, + properties: properties + } as IMetadata; + + if (uri !== null) { + metadata.uri = uri; + } + + return addToGridFS(detectedName, readable, mime, metadata); +}; + +/** + * Add file to drive + * + * @param user User who wish to add file + * @param file File path or readableStream + * @param comment Comment + * @param type File type + * @param folderId Folder ID + * @param force If set to true, forcibly upload the file even if there is a file with the same hash. + * @return Object that represents added file + */ +export default (user: any, file: string | stream.Readable, ...args) => new Promise((resolve, reject) => { + // Get file path + new Promise((res: (v: [string, boolean]) => void, rej) => { + if (typeof file === 'string') { + res([file, false]); + return; + } + if (typeof file === 'object' && typeof file.read === 'function') { + tmpFile() + .then(path => { + const readable: stream.Readable = file; + const writable = fs.createWriteStream(path); + readable + .on('error', rej) + .on('end', () => { + res([path, true]); + }) + .pipe(writable) + .on('error', rej); + }) + .catch(rej); + } + rej(new Error('un-compatible file.')); + }) + .then(([path, shouldCleanup]): Promise => new Promise((res, rej) => { + addFile(user, path, ...args) + .then(file => { + res(file); + if (shouldCleanup) { + fs.unlink(path, (e) => { + if (e) log(e.stack); + }); + } + }) + .catch(rej); + })) + .then(file => { + log(`drive file has been created ${file._id}`); + resolve(file); + + pack(file).then(serializedFile => { + // Publish drive_file_created event + event(user._id, 'drive_file_created', serializedFile); + publishDriveStream(user._id, 'file_created', serializedFile); + + // Register to search database + if (config.elasticsearch.enable) { + const es = require('../db/elasticsearch'); + es.index({ + index: 'misskey', + type: 'drive_file', + id: file._id.toString(), + body: { + name: file.name, + userId: user._id.toString() + } + }); + } + }); + }) + .catch(reject); +}); diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts new file mode 100644 index 0000000000..676586cd15 --- /dev/null +++ b/src/services/drive/upload-from-url.ts @@ -0,0 +1,52 @@ +import * as URL from 'url'; +import { IDriveFile, validateFileName } from '../../models/drive-file'; +import create from './add-file'; +import * as debug from 'debug'; +import * as tmp from 'tmp'; +import * as fs from 'fs'; +import * as request from 'request'; + +const log = debug('misskey:drive:upload-from-url'); + +export default async (url, user, folderId = null, uri = null): Promise => { + log(`REQUESTED: ${url}`); + + let name = URL.parse(url).pathname.split('/').pop(); + if (!validateFileName(name)) { + name = null; + } + + log(`name: ${name}`); + + // Create temp file + const path = await new Promise((res: (string) => void, rej) => { + tmp.file((e, path) => { + if (e) return rej(e); + res(path); + }); + }); + + // write content at URL to temp file + await new Promise((res, rej) => { + const writable = fs.createWriteStream(path); + request(url) + .on('error', rej) + .on('end', () => { + writable.close(); + res(path); + }) + .pipe(writable) + .on('error', rej); + }); + + const driveFile = await create(user, path, name, null, folderId, false, uri); + + log(`created: ${driveFile._id}`); + + // clean-up + fs.unlink(path, (e) => { + if (e) log(e.stack); + }); + + return driveFile; +}; diff --git a/src/services/following/create.ts b/src/services/following/create.ts new file mode 100644 index 0000000000..d919f4487f --- /dev/null +++ b/src/services/following/create.ts @@ -0,0 +1,72 @@ +import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; +import Following from '../../models/following'; +import FollowingLog from '../../models/following-log'; +import FollowedLog from '../../models/followed-log'; +import event from '../../publishers/stream'; +import notify from '../../publishers/notify'; +import context from '../../remote/activitypub/renderer/context'; +import renderFollow from '../../remote/activitypub/renderer/follow'; +import renderAccept from '../../remote/activitypub/renderer/accept'; +import { deliver } from '../../queue'; + +export default async function(follower: IUser, followee: IUser, activity?) { + const following = await Following.insert({ + createdAt: new Date(), + followerId: follower._id, + followeeId: followee._id + }); + + //#region Increment following count + User.update({ _id: follower._id }, { + $inc: { + followingCount: 1 + } + }); + + FollowingLog.insert({ + createdAt: following.createdAt, + userId: follower._id, + count: follower.followingCount + 1 + }); + //#endregion + + //#region Increment followers count + User.update({ _id: followee._id }, { + $inc: { + followersCount: 1 + } + }); + FollowedLog.insert({ + createdAt: following.createdAt, + userId: followee._id, + count: followee.followersCount + 1 + }); + //#endregion + + // Publish follow event + if (isLocalUser(follower)) { + packUser(followee, follower).then(packed => event(follower._id, 'follow', packed)); + } + + // Publish followed event + if (isLocalUser(followee)) { + packUser(follower, followee).then(packed => event(followee._id, 'followed', packed)), + + // 通知を作成 + notify(followee._id, follower._id, 'follow'); + } + + if (isLocalUser(follower) && isRemoteUser(followee)) { + const content = renderFollow(follower, followee); + content['@context'] = context; + + deliver(follower, content, followee.account.inbox).save(); + } + + if (isRemoteUser(follower) && isLocalUser(followee)) { + const content = renderAccept(activity); + content['@context'] = context; + + deliver(followee, content, follower.account.inbox).save(); + } +} diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts new file mode 100644 index 0000000000..364a4803b9 --- /dev/null +++ b/src/services/following/delete.ts @@ -0,0 +1,64 @@ +import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; +import Following from '../../models/following'; +import FollowingLog from '../../models/following-log'; +import FollowedLog from '../../models/followed-log'; +import event from '../../publishers/stream'; +import context from '../../remote/activitypub/renderer/context'; +import renderFollow from '../../remote/activitypub/renderer/follow'; +import renderUndo from '../../remote/activitypub/renderer/undo'; +import { deliver } from '../../queue'; + +export default async function(follower: IUser, followee: IUser, activity?) { + const following = await Following.findOne({ + followerId: follower._id, + followeeId: followee._id + }); + + if (following == null) { + console.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); + return; + } + + Following.remove({ + _id: following._id + }); + + //#region Decrement following count + User.update({ _id: follower._id }, { + $inc: { + followingCount: -1 + } + }); + + FollowingLog.insert({ + createdAt: following.createdAt, + userId: follower._id, + count: follower.followingCount - 1 + }); + //#endregion + + //#region Decrement followers count + User.update({ _id: followee._id }, { + $inc: { + followersCount: -1 + } + }); + FollowedLog.insert({ + createdAt: following.createdAt, + userId: followee._id, + count: followee.followersCount - 1 + }); + //#endregion + + // Publish unfollow event + if (isLocalUser(follower)) { + packUser(followee, follower).then(packed => event(follower._id, 'unfollow', packed)); + } + + if (isLocalUser(follower) && isRemoteUser(followee)) { + const content = renderUndo(renderFollow(follower, followee)); + content['@context'] = context; + + deliver(follower, content, followee.account.inbox).save(); + } +} diff --git a/src/services/post/create.ts b/src/services/post/create.ts new file mode 100644 index 0000000000..9723dbe452 --- /dev/null +++ b/src/services/post/create.ts @@ -0,0 +1,344 @@ +import Post, { pack, IPost } from '../../models/post'; +import User, { isLocalUser, IUser } from '../../models/user'; +import stream from '../../publishers/stream'; +import Following from '../../models/following'; +import { deliver } from '../../queue'; +import renderNote from '../../remote/activitypub/renderer/note'; +import renderCreate from '../../remote/activitypub/renderer/create'; +import context from '../../remote/activitypub/renderer/context'; +import { IDriveFile } from '../../models/drive-file'; +import notify from '../../publishers/notify'; +import PostWatching from '../../models/post-watching'; +import watch from './watch'; +import Mute from '../../models/mute'; +import pushSw from '../../publishers/push-sw'; +import event from '../../publishers/stream'; +import parse from '../../text/parse'; +import html from '../../text/html'; +import { IApp } from '../../models/app'; + +export default async (user: IUser, content: { + createdAt?: Date; + text?: string; + reply?: IPost; + repost?: IPost; + media?: IDriveFile[]; + geo?: any; + poll?: any; + viaMobile?: boolean; + tags?: string[]; + cw?: string; + visibility?: string; + uri?: string; + app?: IApp; +}, silent = false) => new Promise(async (res, rej) => { + if (content.createdAt == null) content.createdAt = new Date(); + if (content.visibility == null) content.visibility = 'public'; + + const tags = content.tags || []; + + let tokens = null; + + if (content.text) { + // Analyze + tokens = parse(content.text); + + // Extract hashtags + const hashtags = tokens + .filter(t => t.type == 'hashtag') + .map(t => t.hashtag); + + hashtags.forEach(tag => { + if (tags.indexOf(tag) == -1) { + tags.push(tag); + } + }); + } + + const data: any = { + createdAt: content.createdAt, + mediaIds: content.media ? content.media.map(file => file._id) : [], + replyId: content.reply ? content.reply._id : null, + repostId: content.repost ? content.repost._id : null, + text: content.text, + textHtml: tokens === null ? null : html(tokens), + poll: content.poll, + cw: content.cw, + tags, + userId: user._id, + viaMobile: content.viaMobile, + geo: content.geo || null, + appId: content.app ? content.app._id : null, + visibility: content.visibility, + + // 以下非正規化データ + _reply: content.reply ? { userId: content.reply.userId } : null, + _repost: content.repost ? { userId: content.repost.userId } : null, + }; + + if (content.uri != null) data.uri = content.uri; + + // 投稿を作成 + const post = await Post.insert(data); + + res(post); + + User.update({ _id: user._id }, { + // Increment posts count + $inc: { + postsCount: 1 + }, + // Update latest post + $set: { + latestPost: post + } + }); + + // Serialize + const postObj = await pack(post); + + // タイムラインへの投稿 + if (!post.channelId) { + // Publish event to myself's stream + if (isLocalUser(user)) { + stream(post.userId, 'post', postObj); + } + + // Fetch all followers + const followers = await Following.aggregate([{ + $lookup: { + from: 'users', + localField: 'followerId', + foreignField: '_id', + as: 'follower' + } + }, { + $match: { + followeeId: post.userId + } + }], { + _id: false + }); + + if (!silent) { + const note = await renderNote(user, post); + const content = renderCreate(note); + content['@context'] = context; + + Promise.all(followers.map(({ follower }) => { + if (isLocalUser(follower)) { + // Publish event to followers stream + stream(follower._id, 'post', postObj); + } else { + // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 + if (isLocalUser(user)) { + deliver(user, content, follower.account.inbox).save(); + } + } + })); + } + } + + // チャンネルへの投稿 + /* TODO + if (post.channelId) { + promises.push( + // Increment channel index(posts count) + Channel.update({ _id: post.channelId }, { + $inc: { + index: 1 + } + }), + + // Publish event to channel + promisedPostObj.then(postObj => { + publishChannelStream(post.channelId, 'post', postObj); + }), + + Promise.all([ + promisedPostObj, + + // Get channel watchers + ChannelWatching.find({ + channelId: post.channelId, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }) + ]).then(([postObj, watches]) => { + // チャンネルの視聴者(のタイムライン)に配信 + watches.forEach(w => { + stream(w.userId, 'post', postObj); + }); + }) + ); + }*/ + + const mentions = []; + + async function addMention(mentionee, reason) { + // Reject if already added + if (mentions.some(x => x.equals(mentionee))) return; + + // Add mention + mentions.push(mentionee); + + // Publish event + if (!user._id.equals(mentionee)) { + const mentioneeMutes = await Mute.find({ + muter_id: mentionee, + deleted_at: { $exists: false } + }); + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); + if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) { + event(mentionee, reason, postObj); + pushSw(mentionee, reason, postObj); + } + } + } + + // If has in reply to post + if (content.reply) { + // Increment replies count + Post.update({ _id: content.reply._id }, { + $inc: { + repliesCount: 1 + } + }); + + // (自分自身へのリプライでない限りは)通知を作成 + notify(content.reply.userId, user._id, 'reply', { + postId: post._id + }); + + // Fetch watchers + PostWatching.find({ + postId: content.reply._id, + userId: { $ne: user._id }, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }, { + fields: { + userId: true + } + }).then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, 'reply', { + postId: post._id + }); + }); + }); + + // この投稿をWatchする + if (isLocalUser(user) && user.account.settings.autoWatch !== false) { + watch(user._id, content.reply); + } + + // Add mention + addMention(content.reply.userId, 'reply'); + } + + // If it is repost + if (content.repost) { + // Notify + const type = content.text ? 'quote' : 'repost'; + notify(content.repost.userId, user._id, type, { + post_id: post._id + }); + + // Fetch watchers + PostWatching.find({ + postId: content.repost._id, + userId: { $ne: user._id }, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }, { + fields: { + userId: true + } + }).then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, type, { + postId: post._id + }); + }); + }); + + // この投稿をWatchする + if (isLocalUser(user) && user.account.settings.autoWatch !== false) { + watch(user._id, content.repost); + } + + // If it is quote repost + if (content.text) { + // Add mention + addMention(content.repost.userId, 'quote'); + } else { + // Publish event + if (!user._id.equals(content.repost.userId)) { + event(content.repost.userId, 'repost', postObj); + } + } + + // 今までで同じ投稿をRepostしているか + const existRepost = await Post.findOne({ + userId: user._id, + repostId: content.repost._id, + _id: { + $ne: post._id + } + }); + + if (!existRepost) { + // Update repostee status + Post.update({ _id: content.repost._id }, { + $inc: { + repostCount: 1 + } + }); + } + } + + // If has text content + if (content.text) { + // Extract an '@' mentions + const atMentions = tokens + .filter(t => t.type == 'mention') + .map(m => m.username) + // Drop dupulicates + .filter((v, i, s) => s.indexOf(v) == i); + + // Resolve all mentions + await Promise.all(atMentions.map(async mention => { + // Fetch mentioned user + // SELECT _id + const mentionee = await User + .findOne({ + usernameLower: mention.toLowerCase() + }, { _id: true }); + + // When mentioned user not found + if (mentionee == null) return; + + // 既に言及されたユーザーに対する返信や引用repostの場合も無視 + if (content.reply && content.reply.userId.equals(mentionee._id)) return; + if (content.repost && content.repost.userId.equals(mentionee._id)) return; + + // Add mention + addMention(mentionee._id, 'mention'); + + // Create notification + notify(mentionee._id, user._id, 'mention', { + post_id: post._id + }); + })); + } + + // Append mentions data + if (mentions.length > 0) { + Post.update({ _id: post._id }, { + $set: { + mentions + } + }); + } +}); diff --git a/src/services/post/reaction/create.ts b/src/services/post/reaction/create.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/services/post/watch.ts b/src/services/post/watch.ts new file mode 100644 index 0000000000..bbd9976f40 --- /dev/null +++ b/src/services/post/watch.ts @@ -0,0 +1,26 @@ +import * as mongodb from 'mongodb'; +import Watching from '../../models/post-watching'; + +export default async (me: mongodb.ObjectID, post: object) => { + // 自分の投稿はwatchできない + if (me.equals((post as any).userId)) { + return; + } + + // if watching now + const exist = await Watching.findOne({ + postId: (post as any)._id, + userId: me, + deletedAt: { $exists: false } + }); + + if (exist !== null) { + return; + } + + await Watching.insert({ + createdAt: new Date(), + postId: (post as any)._id, + userId: me + }); +}; -- cgit v1.2.3-freya From 0154e44e1d02829e8f35fa131005448f694e745e Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 6 Apr 2018 03:42:55 +0900 Subject: Fix bugs --- src/queue/processors/http/index.ts | 3 ++- src/services/post/create.ts | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts index 3d7d941b1a..61d7f9ac94 100644 --- a/src/queue/processors/http/index.ts +++ b/src/queue/processors/http/index.ts @@ -12,8 +12,9 @@ export default (job, done) => { const handler = handlers[job.data.type]; if (handler) { - handler(job).then(() => done(), done); + handler(job, done); } else { console.warn(`Unknown job: ${job.data.type}`); + done(); } }; diff --git a/src/services/post/create.ts b/src/services/post/create.ts index 9723dbe452..405e4a2f7b 100644 --- a/src/services/post/create.ts +++ b/src/services/post/create.ts @@ -98,7 +98,7 @@ export default async (user: IUser, content: { const postObj = await pack(post); // タイムラインへの投稿 - if (!post.channelId) { + if (post.channelId == null) { // Publish event to myself's stream if (isLocalUser(user)) { stream(post.userId, 'post', postObj); @@ -110,7 +110,7 @@ export default async (user: IUser, content: { from: 'users', localField: 'followerId', foreignField: '_id', - as: 'follower' + as: 'user' } }, { $match: { @@ -125,7 +125,9 @@ export default async (user: IUser, content: { const content = renderCreate(note); content['@context'] = context; - Promise.all(followers.map(({ follower }) => { + Promise.all(followers.map(follower => { + follower = follower.user[0]; + if (isLocalUser(follower)) { // Publish event to followers stream stream(follower._id, 'post', postObj); -- cgit v1.2.3-freya From 862463a13cc952919322b4e2f06c196bf2850517 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 6 Apr 2018 14:35:17 +0900 Subject: Fix bug --- src/queue/processors/http/process-inbox.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index c3074429f8..4666e7f373 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -19,6 +19,7 @@ export default async (job: kue.Job, done): Promise => { if (host === null) { console.warn(`request was made by local user: @${username}`); done(); + return; } user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser; @@ -40,7 +41,8 @@ export default async (job: kue.Job, done): Promise => { } if (!verifySignature(signature, user.account.publicKey.publicKeyPem)) { - done(new Error('signature verification failed')); + console.warn('signature verification failed'); + done(); return; } -- cgit v1.2.3-freya From 0856f4cd12ba2e053ab3ef30acdbec7e9a59fc61 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 6 Apr 2018 14:39:28 +0900 Subject: Use error instaed of warn --- src/queue/processors/http/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts index 61d7f9ac94..3dc2595374 100644 --- a/src/queue/processors/http/index.ts +++ b/src/queue/processors/http/index.ts @@ -14,7 +14,7 @@ export default (job, done) => { if (handler) { handler(job, done); } else { - console.warn(`Unknown job: ${job.data.type}`); + console.error(`Unknown job: ${job.data.type}`); done(); } }; -- cgit v1.2.3-freya From 3cf6eab11945e5b2577ef67117a51a210529e456 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 6 Apr 2018 22:40:06 +0900 Subject: Log --- src/queue/processors/http/process-inbox.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index 4666e7f373..eb4b62d37f 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -1,4 +1,5 @@ import * as kue from 'kue'; +import * as debug from 'debug'; import { verifySignature } from 'http-signature'; import parseAcct from '../../../acct/parse'; @@ -6,11 +7,20 @@ import User, { IRemoteUser } from '../../../models/user'; import act from '../../../remote/activitypub/act'; import resolvePerson from '../../../remote/activitypub/resolve-person'; +const log = debug('misskey:queue:inbox'); + // ユーザーのinboxにアクティビティが届いた時の処理 export default async (job: kue.Job, done): Promise => { const signature = job.data.signature; const activity = job.data.activity; + //#region Log + const info = Object.assign({}, activity); + delete info['@context']; + delete info['signature']; + log(info); + //#endregion + const keyIdLower = signature.keyId.toLowerCase(); let user; -- cgit v1.2.3-freya From 66346495e56e07a5b167a8f94ed971f081c94e88 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Apr 2018 06:22:03 +0900 Subject: Fix bug --- src/queue/processors/http/deliver.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts index da7e8bc368..f5d162fd0c 100644 --- a/src/queue/processors/http/deliver.ts +++ b/src/queue/processors/http/deliver.ts @@ -6,9 +6,14 @@ export default async (job: kue.Job, done): Promise => { try { await request(job.data.user, job.data.to, job.data.content); done(); - } catch (e) { - console.warn(`deliver failed: ${e}`); - - done(e); + } catch (res) { + if (res.statusCode >= 300 && res.statusCode < 400) { + // HTTPステータスコード4xxはクライアントエラーであり、それはつまり + // 何回再送しても成功することはないということなのでエラーにはしないでおく + done(); + } else { + console.warn(`deliver failed: ${res.statusMessage}`); + done(new Error(res.statusMessage)); + } } }; -- cgit v1.2.3-freya From 866797a0058a0eac1e5c1d7f6cdeeab845a505ae Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Apr 2018 06:23:38 +0900 Subject: Fix bug --- src/queue/processors/http/deliver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts index f5d162fd0c..422e355b5f 100644 --- a/src/queue/processors/http/deliver.ts +++ b/src/queue/processors/http/deliver.ts @@ -7,7 +7,7 @@ export default async (job: kue.Job, done): Promise => { await request(job.data.user, job.data.to, job.data.content); done(); } catch (res) { - if (res.statusCode >= 300 && res.statusCode < 400) { + if (res.statusCode >= 400 && res.statusCode < 500) { // HTTPステータスコード4xxはクライアントエラーであり、それはつまり // 何回再送しても成功することはないということなのでエラーにはしないでおく done(); -- cgit v1.2.3-freya From 494597236cea4a40bddd9655b6506464df053bfe Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Apr 2018 07:19:30 +0900 Subject: 投稿に関しては論理削除するように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 処理をシンプルにするため --- src/models/post.ts | 1 + src/queue/index.ts | 7 ------- src/queue/processors/db/delete-post-dependents.ts | 22 ---------------------- src/queue/processors/db/index.ts | 7 ------- src/remote/activitypub/act/delete/note.ts | 16 +++++++++------- 5 files changed, 10 insertions(+), 43 deletions(-) delete mode 100644 src/queue/processors/db/delete-post-dependents.ts delete mode 100644 src/queue/processors/db/index.ts (limited to 'src/queue/processors') diff --git a/src/models/post.ts b/src/models/post.ts index 68a638fa2f..ac7890d2e6 100644 --- a/src/models/post.ts +++ b/src/models/post.ts @@ -27,6 +27,7 @@ export type IPost = { _id: mongo.ObjectID; channelId: mongo.ObjectID; createdAt: Date; + deletedAt: Date; mediaIds: mongo.ObjectID[]; replyId: mongo.ObjectID; repostId: mongo.ObjectID; diff --git a/src/queue/index.ts b/src/queue/index.ts index 691223de2d..4aa1dc032d 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -1,7 +1,6 @@ import { createQueue } from 'kue'; import config from '../config'; -import db from './processors/db'; import http from './processors/http'; const queue = createQueue({ @@ -19,10 +18,6 @@ export function createHttp(data) { .backoff({ delay: 16384, type: 'exponential' }); } -export function createDb(data) { - return queue.create('db', data); -} - export function deliver(user, content, to) { return createHttp({ type: 'deliver', @@ -33,8 +28,6 @@ export function deliver(user, content, to) { } export default function() { - queue.process('db', db); - /* 256 is the default concurrency limit of Mozilla Firefox and Google Chromium. diff --git a/src/queue/processors/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts deleted file mode 100644 index 6de21eb053..0000000000 --- a/src/queue/processors/db/delete-post-dependents.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Favorite from '../../../models/favorite'; -import Notification from '../../../models/notification'; -import PollVote from '../../../models/poll-vote'; -import PostReaction from '../../../models/post-reaction'; -import PostWatching from '../../../models/post-watching'; -import Post from '../../../models/post'; - -export default async ({ data }) => Promise.all([ - Favorite.remove({ postId: data._id }), - Notification.remove({ postId: data._id }), - PollVote.remove({ postId: data._id }), - PostReaction.remove({ postId: data._id }), - PostWatching.remove({ postId: data._id }), - Post.find({ repostId: data._id }).then(reposts => Promise.all([ - Notification.remove({ - postId: { - $in: reposts.map(({ _id }) => _id) - } - }), - Post.remove({ repostId: data._id }) - ])) -]); diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts deleted file mode 100644 index 75838c099b..0000000000 --- a/src/queue/processors/db/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import deletePostDependents from './delete-post-dependents'; - -const handlers = { - deletePostDependents -}; - -export default (job, done) => handlers[job.data.type](job).then(() => done(), done); diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/act/delete/note.ts index ff9a8ee5f8..8e9447b481 100644 --- a/src/remote/activitypub/act/delete/note.ts +++ b/src/remote/activitypub/act/delete/note.ts @@ -1,7 +1,6 @@ import * as debug from 'debug'; import Post from '../../../../models/post'; -import { createDb } from '../../../../queue'; import { IRemoteUser } from '../../../../models/user'; const log = debug('misskey:activitypub'); @@ -19,10 +18,13 @@ export default async function(actor: IRemoteUser, uri: string): Promise { throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); } - Post.remove({ _id: post._id }); - - createDb({ - type: 'deletePostDependents', - id: post._id - }).delay(65536).save(); + Post.update({ _id: post._id }, { + $set: { + deletedAt: new Date(), + text: null, + textHtml: null, + mediaIds: [], + poll: null + } + }); } -- cgit v1.2.3-freya From c77013ab3ec748ccc441c497cd04231398194cc6 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Apr 2018 15:45:03 +0900 Subject: oops --- src/queue/processors/http/index.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts index 0ea79305c6..3dc2595374 100644 --- a/src/queue/processors/http/index.ts +++ b/src/queue/processors/http/index.ts @@ -1,17 +1,20 @@ -import deliverPost from './deliver-post'; -import follow from './follow'; -import performActivityPub from './perform-activitypub'; +import deliver from './deliver'; import processInbox from './process-inbox'; import reportGitHubFailure from './report-github-failure'; -import unfollow from './unfollow'; const handlers = { - deliverPost, - follow, - performActivityPub, - processInbox, - reportGitHubFailure, - unfollow + deliver, + processInbox, + reportGitHubFailure }; -export default (job, done) => handlers[job.data.type](job, done); +export default (job, done) => { + const handler = handlers[job.data.type]; + + if (handler) { + handler(job, done); + } else { + console.error(`Unknown job: ${job.data.type}`); + done(); + } +}; -- cgit v1.2.3-freya From a1b490afa756a71b9cef4afa424575bc223bc612 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 02:30:37 +0900 Subject: Post --> Note Closes #1411 --- locales/en.yml | 86 +-- locales/ja.yml | 74 +-- src/client/app/auth/views/form.vue | 2 +- src/client/app/ch/tags/channel.tag | 68 +-- src/client/app/common/mios.ts | 4 +- .../app/common/scripts/compose-notification.ts | 10 +- .../app/common/scripts/parse-search-query.ts | 4 +- src/client/app/common/views/components/index.ts | 4 +- .../views/components/messaging-room.message.vue | 2 +- .../app/common/views/components/note-html.ts | 157 ++++++ .../app/common/views/components/note-menu.vue | 141 +++++ src/client/app/common/views/components/poll.vue | 8 +- .../app/common/views/components/post-html.ts | 157 ------ .../app/common/views/components/post-menu.vue | 141 ----- .../common/views/components/reaction-picker.vue | 6 +- .../common/views/components/reactions-viewer.vue | 4 +- .../common/views/components/welcome-timeline.vue | 26 +- src/client/app/desktop/api/post.ts | 8 +- src/client/app/desktop/script.ts | 16 +- .../desktop/views/components/activity.calendar.vue | 2 +- .../desktop/views/components/activity.chart.vue | 16 +- src/client/app/desktop/views/components/index.ts | 32 +- .../app/desktop/views/components/mentions.vue | 24 +- .../desktop/views/components/note-detail.sub.vue | 130 +++++ .../app/desktop/views/components/note-detail.vue | 448 ++++++++++++++++ .../app/desktop/views/components/note-preview.vue | 107 ++++ .../desktop/views/components/notes.note.sub.vue | 116 ++++ .../app/desktop/views/components/notes.note.vue | 596 +++++++++++++++++++++ src/client/app/desktop/views/components/notes.vue | 89 +++ .../app/desktop/views/components/notifications.vue | 54 +- .../desktop/views/components/post-detail.sub.vue | 130 ----- .../app/desktop/views/components/post-detail.vue | 84 +-- .../desktop/views/components/post-form-window.vue | 6 +- .../app/desktop/views/components/post-form.vue | 32 +- .../app/desktop/views/components/post-preview.vue | 107 ---- .../desktop/views/components/posts.post.sub.vue | 116 ---- .../app/desktop/views/components/posts.post.vue | 104 ++-- src/client/app/desktop/views/components/posts.vue | 89 --- .../views/components/renote-form-window.vue | 42 ++ .../app/desktop/views/components/renote-form.vue | 131 +++++ .../views/components/repost-form-window.vue | 6 +- .../app/desktop/views/components/repost-form.vue | 26 +- .../desktop/views/components/sub-note-content.vue | 44 ++ .../desktop/views/components/sub-post-content.vue | 44 -- .../app/desktop/views/components/timeline.vue | 42 +- .../desktop/views/components/ui.header.post.vue | 6 +- .../app/desktop/views/components/user-preview.vue | 2 +- src/client/app/desktop/views/pages/home.vue | 12 +- src/client/app/desktop/views/pages/note.vue | 67 +++ src/client/app/desktop/views/pages/post.vue | 67 --- src/client/app/desktop/views/pages/search.vue | 32 +- .../app/desktop/views/pages/user/user.home.vue | 2 +- .../app/desktop/views/pages/user/user.photos.vue | 8 +- .../app/desktop/views/pages/user/user.profile.vue | 2 +- .../app/desktop/views/pages/user/user.timeline.vue | 24 +- .../desktop/views/widgets/channel.channel.form.vue | 4 +- .../desktop/views/widgets/channel.channel.note.vue | 75 +++ .../desktop/views/widgets/channel.channel.post.vue | 75 --- .../app/desktop/views/widgets/channel.channel.vue | 36 +- src/client/app/desktop/views/widgets/polls.vue | 8 +- src/client/app/desktop/views/widgets/post-form.vue | 4 +- src/client/app/desktop/views/widgets/trends.vue | 24 +- src/client/app/dev/views/new-app.vue | 2 +- src/client/app/mobile/api/post.ts | 20 +- src/client/app/mobile/script.ts | 4 +- .../app/mobile/views/components/activity.vue | 16 +- src/client/app/mobile/views/components/index.ts | 24 +- .../app/mobile/views/components/note-card.vue | 93 ++++ .../mobile/views/components/note-detail.sub.vue | 113 ++++ .../app/mobile/views/components/note-detail.vue | 462 ++++++++++++++++ .../app/mobile/views/components/note-preview.vue | 110 ++++ .../app/mobile/views/components/note.sub.vue | 119 ++++ src/client/app/mobile/views/components/note.vue | 540 +++++++++++++++++++ src/client/app/mobile/views/components/notes.vue | 111 ++++ .../views/components/notification-preview.vue | 42 +- .../app/mobile/views/components/notification.vue | 38 +- .../app/mobile/views/components/post-card.vue | 93 ---- .../mobile/views/components/post-detail.sub.vue | 113 ---- .../app/mobile/views/components/post-detail.vue | 80 +-- .../app/mobile/views/components/post-form.vue | 10 +- .../app/mobile/views/components/post-preview.vue | 110 ---- .../app/mobile/views/components/post.sub.vue | 119 ---- src/client/app/mobile/views/components/post.vue | 90 ++-- src/client/app/mobile/views/components/posts.vue | 111 ---- .../mobile/views/components/sub-note-content.vue | 43 ++ .../mobile/views/components/sub-post-content.vue | 43 -- .../app/mobile/views/components/timeline.vue | 38 +- .../app/mobile/views/components/user-timeline.vue | 32 +- src/client/app/mobile/views/pages/home.vue | 12 +- src/client/app/mobile/views/pages/note.vue | 85 +++ src/client/app/mobile/views/pages/post.vue | 85 --- src/client/app/mobile/views/pages/search.vue | 30 +- src/client/app/mobile/views/pages/user.vue | 8 +- .../app/mobile/views/pages/user/home.notes.vue | 57 ++ .../app/mobile/views/pages/user/home.photos.vue | 12 +- .../app/mobile/views/pages/user/home.posts.vue | 57 -- src/client/app/mobile/views/pages/user/home.vue | 14 +- src/client/app/stats/tags/index.tag | 28 +- src/client/docs/api/endpoints/notes/create.yaml | 59 ++ src/client/docs/api/endpoints/notes/timeline.yaml | 32 ++ src/client/docs/api/endpoints/posts/create.yaml | 59 -- src/client/docs/api/endpoints/posts/timeline.yaml | 32 -- src/client/docs/api/entities/note.yaml | 174 ++++++ src/client/docs/api/entities/post.yaml | 40 +- src/client/docs/api/entities/user.yaml | 16 +- src/client/docs/mute.ja.pug | 2 +- src/client/docs/search.ja.pug | 16 +- src/models/favorite.ts | 2 +- src/models/note-reaction.ts | 53 ++ src/models/note-watching.ts | 13 + src/models/note.ts | 241 +++++++++ src/models/notification.ts | 14 +- src/models/poll-vote.ts | 2 +- src/models/post-reaction.ts | 53 -- src/models/post-watching.ts | 13 - src/models/post.ts | 241 --------- src/models/user.ts | 18 +- src/othello/ai/back.ts | 10 +- src/othello/ai/front.ts | 18 +- src/publishers/stream.ts | 6 +- src/queue/processors/http/report-github-failure.ts | 4 +- src/remote/activitypub/act/create/note.ts | 22 +- src/remote/activitypub/act/delete/index.ts | 6 +- src/remote/activitypub/act/delete/note.ts | 12 +- src/remote/activitypub/act/like.ts | 12 +- src/remote/activitypub/renderer/like.ts | 4 +- src/remote/activitypub/renderer/note.ts | 28 +- src/remote/activitypub/resolve-person.ts | 4 +- src/renderers/get-note-summary.ts | 45 ++ src/renderers/get-notification-summary.ts | 16 +- src/renderers/get-post-summary.ts | 45 -- src/renderers/get-user-summary.ts | 2 +- src/server/activitypub/index.ts | 4 +- src/server/activitypub/note.ts | 43 ++ src/server/activitypub/outbox.ts | 8 +- src/server/activitypub/post.ts | 43 -- src/server/api/bot/core.ts | 22 +- src/server/api/bot/interfaces/line.ts | 18 +- src/server/api/endpoints.ts | 54 +- .../api/endpoints/aggregation/notes/reaction.ts | 76 +++ .../api/endpoints/aggregation/notes/reactions.ts | 72 +++ .../api/endpoints/aggregation/notes/reply.ts | 75 +++ .../api/endpoints/aggregation/notes/repost.ts | 75 +++ src/server/api/endpoints/aggregation/posts.ts | 22 +- .../api/endpoints/aggregation/posts/reaction.ts | 76 --- .../api/endpoints/aggregation/posts/reactions.ts | 72 --- .../api/endpoints/aggregation/posts/reply.ts | 75 --- .../api/endpoints/aggregation/posts/repost.ts | 75 --- .../api/endpoints/aggregation/users/activity.ts | 20 +- src/server/api/endpoints/aggregation/users/post.ts | 22 +- .../api/endpoints/aggregation/users/reaction.ts | 2 +- src/server/api/endpoints/app/create.ts | 2 +- src/server/api/endpoints/app/name_id/available.ts | 2 +- src/server/api/endpoints/app/show.ts | 2 +- src/server/api/endpoints/auth/accept.ts | 2 +- src/server/api/endpoints/auth/session/generate.ts | 2 +- src/server/api/endpoints/auth/session/show.ts | 2 +- src/server/api/endpoints/auth/session/userkey.ts | 2 +- src/server/api/endpoints/channels/posts.ts | 10 +- src/server/api/endpoints/i/favorites.ts | 4 +- src/server/api/endpoints/i/pin.ts | 20 +- src/server/api/endpoints/meta.ts | 2 +- src/server/api/endpoints/notes/context.ts | 63 +++ src/server/api/endpoints/notes/create.ts | 251 +++++++++ src/server/api/endpoints/notes/favorites/create.ts | 48 ++ src/server/api/endpoints/notes/favorites/delete.ts | 46 ++ src/server/api/endpoints/notes/mentions.ts | 78 +++ .../api/endpoints/notes/polls/recommendation.ts | 59 ++ src/server/api/endpoints/notes/polls/vote.ts | 115 ++++ src/server/api/endpoints/notes/reactions.ts | 57 ++ src/server/api/endpoints/notes/reactions/create.ts | 47 ++ src/server/api/endpoints/notes/reactions/delete.ts | 60 +++ src/server/api/endpoints/notes/replies.ts | 53 ++ src/server/api/endpoints/notes/reposts.ts | 73 +++ src/server/api/endpoints/notes/search.ts | 364 +++++++++++++ src/server/api/endpoints/notes/show.ts | 32 ++ src/server/api/endpoints/notes/timeline.ts | 132 +++++ src/server/api/endpoints/notes/trend.ts | 79 +++ src/server/api/endpoints/posts.ts | 18 +- src/server/api/endpoints/posts/context.ts | 63 --- src/server/api/endpoints/posts/create.ts | 108 ++-- src/server/api/endpoints/posts/favorites/create.ts | 48 -- src/server/api/endpoints/posts/favorites/delete.ts | 46 -- src/server/api/endpoints/posts/mentions.ts | 78 --- .../api/endpoints/posts/polls/recommendation.ts | 59 -- src/server/api/endpoints/posts/polls/vote.ts | 115 ---- src/server/api/endpoints/posts/reactions.ts | 57 -- src/server/api/endpoints/posts/reactions/create.ts | 47 -- src/server/api/endpoints/posts/reactions/delete.ts | 60 --- src/server/api/endpoints/posts/replies.ts | 53 -- src/server/api/endpoints/posts/reposts.ts | 73 --- src/server/api/endpoints/posts/search.ts | 364 ------------- src/server/api/endpoints/posts/show.ts | 32 -- src/server/api/endpoints/posts/timeline.ts | 132 ----- src/server/api/endpoints/posts/trend.ts | 79 --- src/server/api/endpoints/stats.ts | 12 +- .../users/get_frequently_replied_users.ts | 18 +- src/server/api/endpoints/users/posts.ts | 10 +- src/server/api/private/signup.ts | 2 +- src/server/api/service/github.ts | 2 +- src/server/api/stream/home.ts | 24 +- src/services/note/create.ts | 357 ++++++++++++ src/services/note/reaction/create.ts | 94 ++++ src/services/note/watch.ts | 26 + src/services/post/create.ts | 358 ------------- src/services/post/reaction/create.ts | 94 ---- src/services/post/watch.ts | 26 - tools/migration/nighthike/11.js | 35 ++ 208 files changed, 7945 insertions(+), 5267 deletions(-) create mode 100644 src/client/app/common/views/components/note-html.ts create mode 100644 src/client/app/common/views/components/note-menu.vue delete mode 100644 src/client/app/common/views/components/post-html.ts delete mode 100644 src/client/app/common/views/components/post-menu.vue create mode 100644 src/client/app/desktop/views/components/note-detail.sub.vue create mode 100644 src/client/app/desktop/views/components/note-detail.vue create mode 100644 src/client/app/desktop/views/components/note-preview.vue create mode 100644 src/client/app/desktop/views/components/notes.note.sub.vue create mode 100644 src/client/app/desktop/views/components/notes.note.vue create mode 100644 src/client/app/desktop/views/components/notes.vue delete mode 100644 src/client/app/desktop/views/components/post-detail.sub.vue delete mode 100644 src/client/app/desktop/views/components/post-preview.vue delete mode 100644 src/client/app/desktop/views/components/posts.post.sub.vue delete mode 100644 src/client/app/desktop/views/components/posts.vue create mode 100644 src/client/app/desktop/views/components/renote-form-window.vue create mode 100644 src/client/app/desktop/views/components/renote-form.vue create mode 100644 src/client/app/desktop/views/components/sub-note-content.vue delete mode 100644 src/client/app/desktop/views/components/sub-post-content.vue create mode 100644 src/client/app/desktop/views/pages/note.vue delete mode 100644 src/client/app/desktop/views/pages/post.vue create mode 100644 src/client/app/desktop/views/widgets/channel.channel.note.vue delete mode 100644 src/client/app/desktop/views/widgets/channel.channel.post.vue create mode 100644 src/client/app/mobile/views/components/note-card.vue create mode 100644 src/client/app/mobile/views/components/note-detail.sub.vue create mode 100644 src/client/app/mobile/views/components/note-detail.vue create mode 100644 src/client/app/mobile/views/components/note-preview.vue create mode 100644 src/client/app/mobile/views/components/note.sub.vue create mode 100644 src/client/app/mobile/views/components/note.vue create mode 100644 src/client/app/mobile/views/components/notes.vue delete mode 100644 src/client/app/mobile/views/components/post-card.vue delete mode 100644 src/client/app/mobile/views/components/post-detail.sub.vue delete mode 100644 src/client/app/mobile/views/components/post-preview.vue delete mode 100644 src/client/app/mobile/views/components/post.sub.vue delete mode 100644 src/client/app/mobile/views/components/posts.vue create mode 100644 src/client/app/mobile/views/components/sub-note-content.vue delete mode 100644 src/client/app/mobile/views/components/sub-post-content.vue create mode 100644 src/client/app/mobile/views/pages/note.vue delete mode 100644 src/client/app/mobile/views/pages/post.vue create mode 100644 src/client/app/mobile/views/pages/user/home.notes.vue delete mode 100644 src/client/app/mobile/views/pages/user/home.posts.vue create mode 100644 src/client/docs/api/endpoints/notes/create.yaml create mode 100644 src/client/docs/api/endpoints/notes/timeline.yaml delete mode 100644 src/client/docs/api/endpoints/posts/create.yaml delete mode 100644 src/client/docs/api/endpoints/posts/timeline.yaml create mode 100644 src/client/docs/api/entities/note.yaml create mode 100644 src/models/note-reaction.ts create mode 100644 src/models/note-watching.ts create mode 100644 src/models/note.ts delete mode 100644 src/models/post-reaction.ts delete mode 100644 src/models/post-watching.ts delete mode 100644 src/models/post.ts create mode 100644 src/renderers/get-note-summary.ts delete mode 100644 src/renderers/get-post-summary.ts create mode 100644 src/server/activitypub/note.ts delete mode 100644 src/server/activitypub/post.ts create mode 100644 src/server/api/endpoints/aggregation/notes/reaction.ts create mode 100644 src/server/api/endpoints/aggregation/notes/reactions.ts create mode 100644 src/server/api/endpoints/aggregation/notes/reply.ts create mode 100644 src/server/api/endpoints/aggregation/notes/repost.ts delete mode 100644 src/server/api/endpoints/aggregation/posts/reaction.ts delete mode 100644 src/server/api/endpoints/aggregation/posts/reactions.ts delete mode 100644 src/server/api/endpoints/aggregation/posts/reply.ts delete mode 100644 src/server/api/endpoints/aggregation/posts/repost.ts create mode 100644 src/server/api/endpoints/notes/context.ts create mode 100644 src/server/api/endpoints/notes/create.ts create mode 100644 src/server/api/endpoints/notes/favorites/create.ts create mode 100644 src/server/api/endpoints/notes/favorites/delete.ts create mode 100644 src/server/api/endpoints/notes/mentions.ts create mode 100644 src/server/api/endpoints/notes/polls/recommendation.ts create mode 100644 src/server/api/endpoints/notes/polls/vote.ts create mode 100644 src/server/api/endpoints/notes/reactions.ts create mode 100644 src/server/api/endpoints/notes/reactions/create.ts create mode 100644 src/server/api/endpoints/notes/reactions/delete.ts create mode 100644 src/server/api/endpoints/notes/replies.ts create mode 100644 src/server/api/endpoints/notes/reposts.ts create mode 100644 src/server/api/endpoints/notes/search.ts create mode 100644 src/server/api/endpoints/notes/show.ts create mode 100644 src/server/api/endpoints/notes/timeline.ts create mode 100644 src/server/api/endpoints/notes/trend.ts delete mode 100644 src/server/api/endpoints/posts/context.ts delete mode 100644 src/server/api/endpoints/posts/favorites/create.ts delete mode 100644 src/server/api/endpoints/posts/favorites/delete.ts delete mode 100644 src/server/api/endpoints/posts/mentions.ts delete mode 100644 src/server/api/endpoints/posts/polls/recommendation.ts delete mode 100644 src/server/api/endpoints/posts/polls/vote.ts delete mode 100644 src/server/api/endpoints/posts/reactions.ts delete mode 100644 src/server/api/endpoints/posts/reactions/create.ts delete mode 100644 src/server/api/endpoints/posts/reactions/delete.ts delete mode 100644 src/server/api/endpoints/posts/replies.ts delete mode 100644 src/server/api/endpoints/posts/reposts.ts delete mode 100644 src/server/api/endpoints/posts/search.ts delete mode 100644 src/server/api/endpoints/posts/show.ts delete mode 100644 src/server/api/endpoints/posts/timeline.ts delete mode 100644 src/server/api/endpoints/posts/trend.ts create mode 100644 src/services/note/create.ts create mode 100644 src/services/note/reaction/create.ts create mode 100644 src/services/note/watch.ts delete mode 100644 src/services/post/create.ts delete mode 100644 src/services/post/reaction/create.ts delete mode 100644 src/services/post/watch.ts create mode 100644 tools/migration/nighthike/11.js (limited to 'src/queue/processors') diff --git a/locales/en.yml b/locales/en.yml index 2cc857f698..900571124f 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -33,7 +33,7 @@ common: confused: "Confused" pudding: "Pudding" - post_categories: + note_categories: music: "Music" game: "Video Game" anime: "Anime" @@ -124,7 +124,7 @@ common: show-result: "Show result" voted: "Voted" - mk-post-menu: + mk-note-menu: pin: "Pin" pinned: "Pinned" select: "Select category" @@ -211,7 +211,7 @@ ch: textarea: "Write here" upload: "Upload" drive: "Drive" - post: "Do" + note: "Do" posting: "Doing" desktop: @@ -304,8 +304,8 @@ desktop: settings: "Settings" signout: "Sign out" - mk-ui-header-post-button: - post: "Compose new Post" + mk-ui-header-note-button: + note: "Compose new Post" mk-ui-header-notifications: title: "Notifications" @@ -350,18 +350,18 @@ desktop: no-users: "No muted users" mk-post-form: - post-placeholder: "What's happening?" - reply-placeholder: "Reply to this post..." - quote-placeholder: "Quote this post..." - post: "Post" + note-placeholder: "What's happening?" + reply-placeholder: "Reply to this note..." + quote-placeholder: "Quote this note..." + note: "Post" reply: "Reply" - repost: "Repost" + renote: "Renote" posted: "Posted!" replied: "Replied!" reposted: "Reposted!" - post-failed: "Failed to post" + note-failed: "Failed to note" reply-failed: "Failed to reply" - repost-failed: "Failed to repost" + renote-failed: "Failed to renote" posting: "Posting" attach-media-from-local: "Attach media from your pc" attach-media-from-drive: "Attach media from the drive" @@ -371,14 +371,14 @@ desktop: text-remain: "{} chars remaining" mk-post-form-window: - post: "New post" + note: "New note" reply: "Reply" attaches: "{} media attached" uploading-media: "Uploading {} media" - mk-post-page: - prev: "Previous post" - next: "Next post" + mk-note-page: + prev: "Previous note" + next: "Next note" mk-settings: profile: "Profile" @@ -390,10 +390,10 @@ desktop: other: "Other" license: "License" - mk-timeline-post: + mk-timeline-note: reposted-by: "Reposted by {}" reply: "Reply" - repost: "Repost" + renote: "Renote" add-reaction: "Add your reaction" detail: "Show detail" @@ -448,7 +448,7 @@ desktop: mk-post-form-home-widget: title: "Post" - post: "Post" + note: "Post" placeholder: "What's happening?" mk-access-log-home-widget: @@ -463,16 +463,16 @@ desktop: have-a-nice-day: "Have a nice day!" next: "Next" - mk-repost-form: + mk-renote-form: quote: "Quote..." cancel: "Cancel" - repost: "Repost" + renote: "Renote" reposting: "Reposting..." success: "Reposted!" - failure: "Failed to Repost" + failure: "Failed to Renote" - mk-repost-form-window: - title: "Are you sure you want to repost this post?" + mk-renote-form-window: + title: "Are you sure you want to renote this note?" mk-user: last-used-at: "Last used at" @@ -541,10 +541,10 @@ mobile: notifications: "Notifications" read-all: "Are you sure you want to mark all unread notifications as read?" - mk-post-page: + mk-note-page: title: "Post" - prev: "Previous post" - next: "Next post" + prev: "Previous note" + next: "Next note" mk-search-page: search: "Search" @@ -606,33 +606,33 @@ mobile: unfollow: "Unfollow" mk-home-timeline: - empty-timeline: "There is no posts" + empty-timeline: "There is no notes" mk-notifications: more: "More" empty: "No notifications" - mk-post-detail: + mk-note-detail: reply: "Reply" reaction: "Reaction" mk-post-form: submit: "Post" - reply-placeholder: "Reply to this post..." - post-placeholder: "What's happening?" + reply-placeholder: "Reply to this note..." + note-placeholder: "What's happening?" - mk-search-posts: - empty: "There is no post related to the 「{}」" + mk-search-notes: + empty: "There is no note related to the 「{}」" - mk-sub-post-content: + mk-sub-note-content: media-count: "{} media" poll: "Poll" - mk-timeline-post: + mk-timeline-note: reposted-by: "Reposted by {}" mk-timeline: - empty: "No posts" + empty: "No notes" load-more: "More" mk-ui-nav: @@ -652,21 +652,21 @@ mobile: no-users: "No following." mk-user-timeline: - no-posts: "This user seems never post" - no-posts-with-media: "There is no posts with media" + no-notes: "This user seems never note" + no-notes-with-media: "There is no notes with media" load-more: "More" mk-user: follows-you: "Follows you" following: "Following" followers: "Followers" - posts: "Posts" + notes: "Posts" overview: "Overview" timeline: "Timeline" media: "Media" mk-user-overview: - recent-posts: "Recent posts" + recent-notes: "Recent notes" images: "Images" activity: "Activity" keywords: "Keywords" @@ -675,9 +675,9 @@ mobile: followers-you-know: "Followers you know" last-used-at: "Last used at" - mk-user-overview-posts: + mk-user-overview-notes: loading: "Loading" - no-posts: "No posts" + no-notes: "No notes" mk-user-overview-photos: loading: "Loading" @@ -703,7 +703,7 @@ mobile: load-more: "More" stats: - posts-count: "Number of all posts" + notes-count: "Number of all notes" users-count: "Number of all users" status: diff --git a/locales/ja.yml b/locales/ja.yml index fd140ecc30..84694e3c74 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -33,7 +33,7 @@ common: confused: "こまこまのこまり" pudding: "Pudding" - post_categories: + note_categories: music: "音楽" game: "ゲーム" anime: "アニメ" @@ -124,7 +124,7 @@ common: show-result: "結果を見る" voted: "投票済み" - mk-post-menu: + mk-note-menu: pin: "ピン留め" pinned: "ピン留めしました" select: "カテゴリを選択" @@ -211,7 +211,7 @@ ch: textarea: "書いて" upload: "アップロード" drive: "ドライブ" - post: "やる" + note: "やる" posting: "やってます" desktop: @@ -304,8 +304,8 @@ desktop: settings: "設定" signout: "サインアウト" - mk-ui-header-post-button: - post: "新規投稿" + mk-ui-header-note-button: + note: "新規投稿" mk-ui-header-notifications: title: "通知" @@ -350,18 +350,18 @@ desktop: no-users: "ミュートしているユーザーはいません" mk-post-form: - post-placeholder: "いまどうしてる?" + note-placeholder: "いまどうしてる?" reply-placeholder: "この投稿への返信..." quote-placeholder: "この投稿を引用..." - post: "投稿" + note: "投稿" reply: "返信" - repost: "Repost" + renote: "Renote" posted: "投稿しました!" replied: "返信しました!" - reposted: "Repostしました!" - post-failed: "投稿に失敗しました" + reposted: "Renoteしました!" + note-failed: "投稿に失敗しました" reply-failed: "返信に失敗しました" - repost-failed: "Repostに失敗しました" + renote-failed: "Renoteに失敗しました" posting: "投稿中" attach-media-from-local: "PCからメディアを添付" attach-media-from-drive: "ドライブからメディアを添付" @@ -371,12 +371,12 @@ desktop: text-remain: "のこり{}文字" mk-post-form-window: - post: "新規投稿" + note: "新規投稿" reply: "返信" attaches: "添付: {}メディア" uploading-media: "{}個のメディアをアップロード中" - mk-post-page: + mk-note-page: prev: "前の投稿" next: "次の投稿" @@ -390,10 +390,10 @@ desktop: other: "その他" license: "ライセンス" - mk-timeline-post: - reposted-by: "{}がRepost" + mk-timeline-note: + reposted-by: "{}がRenote" reply: "返信" - repost: "Repost" + renote: "Renote" add-reaction: "リアクション" detail: "詳細" @@ -448,7 +448,7 @@ desktop: mk-post-form-home-widget: title: "投稿" - post: "投稿" + note: "投稿" placeholder: "いまどうしてる?" mk-access-log-home-widget: @@ -463,16 +463,16 @@ desktop: have-a-nice-day: "良い一日を!" next: "次" - mk-repost-form: + mk-renote-form: quote: "引用する..." cancel: "キャンセル" - repost: "Repost" + renote: "Renote" reposting: "しています..." - success: "Repostしました!" - failure: "Repostに失敗しました" + success: "Renoteしました!" + failure: "Renoteに失敗しました" - mk-repost-form-window: - title: "この投稿をRepostしますか?" + mk-renote-form-window: + title: "この投稿をRenoteしますか?" mk-user: last-used-at: "最終アクセス" @@ -541,7 +541,7 @@ mobile: notifications: "通知" read-all: "すべての通知を既読にしますか?" - mk-post-page: + mk-note-page: title: "投稿" prev: "前の投稿" next: "次の投稿" @@ -612,24 +612,24 @@ mobile: more: "もっと見る" empty: "ありません!" - mk-post-detail: + mk-note-detail: reply: "返信" reaction: "リアクション" mk-post-form: submit: "投稿" reply-placeholder: "この投稿への返信..." - post-placeholder: "いまどうしてる?" + note-placeholder: "いまどうしてる?" - mk-search-posts: + mk-search-notes: empty: "「{}」に関する投稿は見つかりませんでした。" - mk-sub-post-content: + mk-sub-note-content: media-count: "{}個のメディア" poll: "投票" - mk-timeline-post: - reposted-by: "{}がRepost" + mk-timeline-note: + reposted-by: "{}がRenote" mk-timeline: empty: "表示するものがありません" @@ -652,21 +652,21 @@ mobile: no-users: "フォロー中のユーザーはいないようです。" mk-user-timeline: - no-posts: "このユーザーはまだ投稿していないようです。" - no-posts-with-media: "メディア付き投稿はありません。" + no-notes: "このユーザーはまだ投稿していないようです。" + no-notes-with-media: "メディア付き投稿はありません。" load-more: "もっとみる" mk-user: follows-you: "フォローされています" following: "フォロー" followers: "フォロワー" - posts: "投稿" + notes: "投稿" overview: "概要" timeline: "タイムライン" media: "メディア" mk-user-overview: - recent-posts: "最近の投稿" + recent-notes: "最近の投稿" images: "画像" activity: "アクティビティ" keywords: "キーワード" @@ -675,9 +675,9 @@ mobile: followers-you-know: "知り合いのフォロワー" last-used-at: "最終ログイン" - mk-user-overview-posts: + mk-user-overview-notes: loading: "読み込み中" - no-posts: "投稿はありません" + no-notes: "投稿はありません" mk-user-overview-photos: loading: "読み込み中" @@ -703,7 +703,7 @@ mobile: load-more: "もっと" stats: - posts-count: "投稿の数" + notes-count: "投稿の数" users-count: "アカウントの数" status: diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue index eb55b9035b..b323907eb0 100644 --- a/src/client/app/auth/views/form.vue +++ b/src/client/app/auth/views/form.vue @@ -16,7 +16,7 @@ @@ -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 08beb45935961066ddc543b1659f5cc6e891aa6c Mon Sep 17 00:00:00 2001 From: syuilo 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/queue/processors') 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 2a5016865ad0ed98ab8da3364b5f006bf26f45c5 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 9 Apr 2018 17:38:22 +0900 Subject: Fix bug --- src/queue/processors/http/index.ts | 2 - src/queue/processors/http/report-github-failure.ts | 24 ----------- src/server/api/service/github.ts | 46 ++++++++++++++-------- 3 files changed, 30 insertions(+), 42 deletions(-) delete mode 100644 src/queue/processors/http/report-github-failure.ts (limited to 'src/queue/processors') diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts index 3dc2595374..6f8d1dbc2b 100644 --- a/src/queue/processors/http/index.ts +++ b/src/queue/processors/http/index.ts @@ -1,11 +1,9 @@ import deliver from './deliver'; import processInbox from './process-inbox'; -import reportGitHubFailure from './report-github-failure'; const handlers = { deliver, processInbox, - reportGitHubFailure }; export default (job, done) => { diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts deleted file mode 100644 index 13f9afadd3..0000000000 --- a/src/queue/processors/http/report-github-failure.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as request from 'request-promise-native'; -import User from '../../../models/user'; -import createNote from '../../../services/note/create'; - -export default async ({ data }) => { - const asyncBot = User.findOne({ _id: data.userId }); - - // Fetch parent status - const parentStatuses = await request({ - url: `${data.parentUrl}/statuses`, - headers: { - 'User-Agent': 'misskey' - }, - json: true - }); - - const parentState = parentStatuses[0].state; - const stillFailed = parentState == 'failure' || parentState == 'error'; - const text = stillFailed ? - `**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` : - `**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`; - - createNote(await asyncBot, { text }); -}; diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index 6a327f1f76..6b1d5d25be 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -1,16 +1,17 @@ import * as EventEmitter from 'events'; import * as express from 'express'; -//const crypto = require('crypto'); +import * as request from 'request'; +const crypto = require('crypto'); + import User from '../../../models/user'; +import createNote from '../../../services/note/create'; import config from '../../../config'; -import { createHttp } from '../../../queue'; module.exports = async (app: express.Application) => { if (config.github_bot == null) return; const bot = await User.findOne({ - usernameLower: config.github_bot.username.toLowerCase(), - host: null + username_lower: config.github_bot.username.toLowerCase() }); if (bot == null) { @@ -18,7 +19,7 @@ module.exports = async (app: express.Application) => { return; } - const post = text => require('../endpoints/notes/create')({ text }, bot); + const post = text => createNote(bot, { text }); const handler = new EventEmitter(); @@ -26,12 +27,12 @@ module.exports = async (app: express.Application) => { // req.headers['x-hub-signature'] および // req.headers['x-github-event'] は常に string ですが、型定義の都合上 // string | string[] になっているので string を明示しています -// if ((new Buffer(req.headers['x-hub-signature'] as string)).equals(new Buffer(`sha1=${crypto.createHmac('sha1', config.github_bot.hook_secret).update(JSON.stringify(req.body)).digest('hex')}`))) { + if ((new Buffer(req.headers['x-hub-signature'] as string)).equals(new Buffer(`sha1=${crypto.createHmac('sha1', config.github_bot.hook_secret).update(JSON.stringify(req.body)).digest('hex')}`))) { handler.emit(req.headers['x-github-event'] as string, req.body); res.sendStatus(200); -// } else { -// res.sendStatus(400); -// } + } else { + res.sendStatus(400); + } }); handler.on('status', event => { @@ -42,13 +43,26 @@ module.exports = async (app: express.Application) => { const commit = event.commit; const parent = commit.parents[0]; - createHttp({ - type: 'gitHubFailureReport', - userId: bot._id, - parentUrl: parent.url, - htmlUrl: commit.html_url, - message: commit.commit.message, - }).save(); + // Fetch parent status + request({ + url: `${parent.url}/statuses`, + headers: { + 'User-Agent': 'misskey' + } + }, (err, res, body) => { + if (err) { + console.error(err); + return; + } + const parentStatuses = JSON.parse(body); + const parentState = parentStatuses[0].state; + const stillFailed = parentState == 'failure' || parentState == 'error'; + if (stillFailed) { + post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`); + } else { + post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`); + } + }); break; } }); -- 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/queue/processors') 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