From 90f8fe7e538bb7e52d2558152a0390e693f39b11 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Thu, 29 Mar 2018 01:20:40 +0900 Subject: Introduce processor --- src/server/api/api-handler.ts | 56 ++ src/server/api/authenticate.ts | 69 +++ src/server/api/bot/core.ts | 438 ++++++++++++++++ src/server/api/bot/interfaces/line.ts | 238 +++++++++ src/server/api/common/drive/add-file.ts | 307 +++++++++++ src/server/api/common/drive/upload_from_url.ts | 46 ++ .../api/common/generate-native-user-token.ts | 3 + src/server/api/common/get-friends.ts | 26 + src/server/api/common/get-host-lower.ts | 5 + src/server/api/common/is-native-token.ts | 1 + src/server/api/common/notify.ts | 50 ++ src/server/api/common/push-sw.ts | 52 ++ src/server/api/common/read-messaging-message.ts | 66 +++ src/server/api/common/read-notification.ts | 52 ++ src/server/api/common/signin.ts | 19 + .../api/common/text/core/syntax-highlighter.ts | 334 ++++++++++++ src/server/api/common/text/elements/bold.ts | 14 + src/server/api/common/text/elements/code.ts | 17 + src/server/api/common/text/elements/emoji.ts | 14 + src/server/api/common/text/elements/hashtag.ts | 19 + src/server/api/common/text/elements/inline-code.ts | 17 + src/server/api/common/text/elements/link.ts | 19 + src/server/api/common/text/elements/mention.ts | 17 + src/server/api/common/text/elements/quote.ts | 14 + src/server/api/common/text/elements/url.ts | 14 + src/server/api/common/text/index.ts | 72 +++ src/server/api/common/watch-post.ts | 26 + src/server/api/endpoints.ts | 584 +++++++++++++++++++++ src/server/api/endpoints/aggregation/posts.ts | 90 ++++ .../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 +++ src/server/api/endpoints/aggregation/users.ts | 61 +++ .../api/endpoints/aggregation/users/activity.ts | 116 ++++ .../api/endpoints/aggregation/users/followers.ts | 74 +++ .../api/endpoints/aggregation/users/following.ts | 73 +++ src/server/api/endpoints/aggregation/users/post.ts | 110 ++++ .../api/endpoints/aggregation/users/reaction.ts | 80 +++ src/server/api/endpoints/app/create.ts | 108 ++++ src/server/api/endpoints/app/name_id/available.ts | 60 +++ src/server/api/endpoints/app/show.ts | 72 +++ src/server/api/endpoints/auth/accept.ts | 93 ++++ src/server/api/endpoints/auth/session/generate.ts | 76 +++ src/server/api/endpoints/auth/session/show.ts | 70 +++ src/server/api/endpoints/auth/session/userkey.ts | 109 ++++ src/server/api/endpoints/channels.ts | 58 ++ src/server/api/endpoints/channels/create.ts | 39 ++ src/server/api/endpoints/channels/posts.ts | 78 +++ src/server/api/endpoints/channels/show.ts | 30 ++ src/server/api/endpoints/channels/unwatch.ts | 60 +++ src/server/api/endpoints/channels/watch.ts | 58 ++ src/server/api/endpoints/drive.ts | 37 ++ src/server/api/endpoints/drive/files.ts | 73 +++ src/server/api/endpoints/drive/files/create.ts | 51 ++ src/server/api/endpoints/drive/files/find.ts | 34 ++ src/server/api/endpoints/drive/files/show.ts | 36 ++ src/server/api/endpoints/drive/files/update.ts | 75 +++ .../api/endpoints/drive/files/upload_from_url.ts | 26 + src/server/api/endpoints/drive/folders.ts | 66 +++ src/server/api/endpoints/drive/folders/create.ts | 55 ++ src/server/api/endpoints/drive/folders/find.ts | 33 ++ src/server/api/endpoints/drive/folders/show.ts | 34 ++ src/server/api/endpoints/drive/folders/update.ts | 99 ++++ src/server/api/endpoints/drive/stream.ts | 67 +++ src/server/api/endpoints/following/create.ts | 84 +++ src/server/api/endpoints/following/delete.ts | 81 +++ src/server/api/endpoints/i.ts | 28 + src/server/api/endpoints/i/2fa/done.ts | 37 ++ src/server/api/endpoints/i/2fa/register.ts | 48 ++ src/server/api/endpoints/i/2fa/unregister.ts | 28 + src/server/api/endpoints/i/appdata/get.ts | 39 ++ src/server/api/endpoints/i/appdata/set.ts | 58 ++ src/server/api/endpoints/i/authorized_apps.ts | 43 ++ src/server/api/endpoints/i/change_password.ts | 42 ++ src/server/api/endpoints/i/favorites.ts | 44 ++ src/server/api/endpoints/i/notifications.ts | 110 ++++ src/server/api/endpoints/i/pin.ts | 44 ++ src/server/api/endpoints/i/regenerate_token.ts | 42 ++ src/server/api/endpoints/i/signin_history.ts | 61 +++ src/server/api/endpoints/i/update.ts | 97 ++++ .../api/endpoints/i/update_client_setting.ts | 43 ++ src/server/api/endpoints/i/update_home.ts | 60 +++ src/server/api/endpoints/i/update_mobile_home.ts | 59 +++ src/server/api/endpoints/messaging/history.ts | 43 ++ src/server/api/endpoints/messaging/messages.ts | 102 ++++ .../api/endpoints/messaging/messages/create.ts | 156 ++++++ src/server/api/endpoints/messaging/unread.ts | 33 ++ src/server/api/endpoints/meta.ts | 59 +++ src/server/api/endpoints/mute/create.ts | 61 +++ src/server/api/endpoints/mute/delete.ts | 63 +++ src/server/api/endpoints/mute/list.ts | 73 +++ src/server/api/endpoints/my/apps.ts | 40 ++ .../endpoints/notifications/get_unread_count.ts | 33 ++ .../endpoints/notifications/mark_as_read_all.ts | 32 ++ src/server/api/endpoints/othello/games.ts | 62 +++ src/server/api/endpoints/othello/games/show.ts | 32 ++ src/server/api/endpoints/othello/invitations.ts | 15 + src/server/api/endpoints/othello/match.ts | 95 ++++ src/server/api/endpoints/othello/match/cancel.ts | 9 + src/server/api/endpoints/posts.ts | 97 ++++ src/server/api/endpoints/posts/categorize.ts | 52 ++ src/server/api/endpoints/posts/context.ts | 63 +++ src/server/api/endpoints/posts/create.ts | 536 +++++++++++++++++++ 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 | 122 +++++ 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 | 48 ++ src/server/api/endpoints/sw/register.ts | 50 ++ src/server/api/endpoints/username/available.ts | 32 ++ src/server/api/endpoints/users.ts | 56 ++ src/server/api/endpoints/users/followers.ts | 92 ++++ src/server/api/endpoints/users/following.ts | 92 ++++ .../users/get_frequently_replied_users.ts | 99 ++++ src/server/api/endpoints/users/posts.ts | 137 +++++ src/server/api/endpoints/users/recommendation.ts | 53 ++ src/server/api/endpoints/users/search.ts | 98 ++++ .../api/endpoints/users/search_by_username.ts | 38 ++ src/server/api/endpoints/users/show.ts | 209 ++++++++ src/server/api/event.ts | 80 +++ src/server/api/limitter.ts | 83 +++ src/server/api/models/access-token.ts | 8 + src/server/api/models/app.ts | 97 ++++ src/server/api/models/appdata.ts | 3 + src/server/api/models/auth-session.ts | 45 ++ src/server/api/models/channel-watching.ts | 3 + src/server/api/models/channel.ts | 74 +++ src/server/api/models/drive-file.ts | 113 ++++ src/server/api/models/drive-folder.ts | 77 +++ src/server/api/models/drive-tag.ts | 3 + src/server/api/models/favorite.ts | 3 + src/server/api/models/following.ts | 3 + src/server/api/models/messaging-history.ts | 3 + src/server/api/models/messaging-message.ts | 81 +++ src/server/api/models/meta.ts | 7 + src/server/api/models/mute.ts | 3 + src/server/api/models/notification.ts | 107 ++++ src/server/api/models/othello-game.ts | 109 ++++ src/server/api/models/othello-matching.ts | 44 ++ src/server/api/models/poll-vote.ts | 3 + src/server/api/models/post-reaction.ts | 51 ++ src/server/api/models/post-watching.ts | 3 + src/server/api/models/post.ts | 219 ++++++++ src/server/api/models/signin.ts | 29 + src/server/api/models/sw-subscription.ts | 3 + src/server/api/models/user.ts | 340 ++++++++++++ src/server/api/private/signin.ts | 91 ++++ src/server/api/private/signup.ts | 165 ++++++ src/server/api/reply.ts | 13 + src/server/api/server.ts | 55 ++ src/server/api/service/github.ts | 124 +++++ src/server/api/service/twitter.ts | 176 +++++++ src/server/api/stream/channel.ts | 12 + src/server/api/stream/drive.ts | 10 + src/server/api/stream/home.ts | 95 ++++ src/server/api/stream/messaging-index.ts | 10 + src/server/api/stream/messaging.ts | 24 + src/server/api/stream/othello-game.ts | 331 ++++++++++++ src/server/api/stream/othello.ts | 29 + src/server/api/stream/requests.ts | 19 + src/server/api/stream/server.ts | 19 + src/server/api/streaming.ts | 118 +++++ 173 files changed, 12999 insertions(+) create mode 100644 src/server/api/api-handler.ts create mode 100644 src/server/api/authenticate.ts create mode 100644 src/server/api/bot/core.ts create mode 100644 src/server/api/bot/interfaces/line.ts create mode 100644 src/server/api/common/drive/add-file.ts create mode 100644 src/server/api/common/drive/upload_from_url.ts create mode 100644 src/server/api/common/generate-native-user-token.ts create mode 100644 src/server/api/common/get-friends.ts create mode 100644 src/server/api/common/get-host-lower.ts create mode 100644 src/server/api/common/is-native-token.ts create mode 100644 src/server/api/common/notify.ts create mode 100644 src/server/api/common/push-sw.ts create mode 100644 src/server/api/common/read-messaging-message.ts create mode 100644 src/server/api/common/read-notification.ts create mode 100644 src/server/api/common/signin.ts create mode 100644 src/server/api/common/text/core/syntax-highlighter.ts create mode 100644 src/server/api/common/text/elements/bold.ts create mode 100644 src/server/api/common/text/elements/code.ts create mode 100644 src/server/api/common/text/elements/emoji.ts create mode 100644 src/server/api/common/text/elements/hashtag.ts create mode 100644 src/server/api/common/text/elements/inline-code.ts create mode 100644 src/server/api/common/text/elements/link.ts create mode 100644 src/server/api/common/text/elements/mention.ts create mode 100644 src/server/api/common/text/elements/quote.ts create mode 100644 src/server/api/common/text/elements/url.ts create mode 100644 src/server/api/common/text/index.ts create mode 100644 src/server/api/common/watch-post.ts create mode 100644 src/server/api/endpoints.ts create mode 100644 src/server/api/endpoints/aggregation/posts.ts create mode 100644 src/server/api/endpoints/aggregation/posts/reaction.ts create mode 100644 src/server/api/endpoints/aggregation/posts/reactions.ts create mode 100644 src/server/api/endpoints/aggregation/posts/reply.ts create mode 100644 src/server/api/endpoints/aggregation/posts/repost.ts create mode 100644 src/server/api/endpoints/aggregation/users.ts create mode 100644 src/server/api/endpoints/aggregation/users/activity.ts create mode 100644 src/server/api/endpoints/aggregation/users/followers.ts create mode 100644 src/server/api/endpoints/aggregation/users/following.ts create mode 100644 src/server/api/endpoints/aggregation/users/post.ts create mode 100644 src/server/api/endpoints/aggregation/users/reaction.ts create mode 100644 src/server/api/endpoints/app/create.ts create mode 100644 src/server/api/endpoints/app/name_id/available.ts create mode 100644 src/server/api/endpoints/app/show.ts create mode 100644 src/server/api/endpoints/auth/accept.ts create mode 100644 src/server/api/endpoints/auth/session/generate.ts create mode 100644 src/server/api/endpoints/auth/session/show.ts create mode 100644 src/server/api/endpoints/auth/session/userkey.ts create mode 100644 src/server/api/endpoints/channels.ts create mode 100644 src/server/api/endpoints/channels/create.ts create mode 100644 src/server/api/endpoints/channels/posts.ts create mode 100644 src/server/api/endpoints/channels/show.ts create mode 100644 src/server/api/endpoints/channels/unwatch.ts create mode 100644 src/server/api/endpoints/channels/watch.ts create mode 100644 src/server/api/endpoints/drive.ts create mode 100644 src/server/api/endpoints/drive/files.ts create mode 100644 src/server/api/endpoints/drive/files/create.ts create mode 100644 src/server/api/endpoints/drive/files/find.ts create mode 100644 src/server/api/endpoints/drive/files/show.ts create mode 100644 src/server/api/endpoints/drive/files/update.ts create mode 100644 src/server/api/endpoints/drive/files/upload_from_url.ts create mode 100644 src/server/api/endpoints/drive/folders.ts create mode 100644 src/server/api/endpoints/drive/folders/create.ts create mode 100644 src/server/api/endpoints/drive/folders/find.ts create mode 100644 src/server/api/endpoints/drive/folders/show.ts create mode 100644 src/server/api/endpoints/drive/folders/update.ts create mode 100644 src/server/api/endpoints/drive/stream.ts create mode 100644 src/server/api/endpoints/following/create.ts create mode 100644 src/server/api/endpoints/following/delete.ts create mode 100644 src/server/api/endpoints/i.ts create mode 100644 src/server/api/endpoints/i/2fa/done.ts create mode 100644 src/server/api/endpoints/i/2fa/register.ts create mode 100644 src/server/api/endpoints/i/2fa/unregister.ts create mode 100644 src/server/api/endpoints/i/appdata/get.ts create mode 100644 src/server/api/endpoints/i/appdata/set.ts create mode 100644 src/server/api/endpoints/i/authorized_apps.ts create mode 100644 src/server/api/endpoints/i/change_password.ts create mode 100644 src/server/api/endpoints/i/favorites.ts create mode 100644 src/server/api/endpoints/i/notifications.ts create mode 100644 src/server/api/endpoints/i/pin.ts create mode 100644 src/server/api/endpoints/i/regenerate_token.ts create mode 100644 src/server/api/endpoints/i/signin_history.ts create mode 100644 src/server/api/endpoints/i/update.ts create mode 100644 src/server/api/endpoints/i/update_client_setting.ts create mode 100644 src/server/api/endpoints/i/update_home.ts create mode 100644 src/server/api/endpoints/i/update_mobile_home.ts create mode 100644 src/server/api/endpoints/messaging/history.ts create mode 100644 src/server/api/endpoints/messaging/messages.ts create mode 100644 src/server/api/endpoints/messaging/messages/create.ts create mode 100644 src/server/api/endpoints/messaging/unread.ts create mode 100644 src/server/api/endpoints/meta.ts create mode 100644 src/server/api/endpoints/mute/create.ts create mode 100644 src/server/api/endpoints/mute/delete.ts create mode 100644 src/server/api/endpoints/mute/list.ts create mode 100644 src/server/api/endpoints/my/apps.ts create mode 100644 src/server/api/endpoints/notifications/get_unread_count.ts create mode 100644 src/server/api/endpoints/notifications/mark_as_read_all.ts create mode 100644 src/server/api/endpoints/othello/games.ts create mode 100644 src/server/api/endpoints/othello/games/show.ts create mode 100644 src/server/api/endpoints/othello/invitations.ts create mode 100644 src/server/api/endpoints/othello/match.ts create mode 100644 src/server/api/endpoints/othello/match/cancel.ts create mode 100644 src/server/api/endpoints/posts.ts create mode 100644 src/server/api/endpoints/posts/categorize.ts create mode 100644 src/server/api/endpoints/posts/context.ts create mode 100644 src/server/api/endpoints/posts/create.ts create mode 100644 src/server/api/endpoints/posts/favorites/create.ts create mode 100644 src/server/api/endpoints/posts/favorites/delete.ts create mode 100644 src/server/api/endpoints/posts/mentions.ts create mode 100644 src/server/api/endpoints/posts/polls/recommendation.ts create mode 100644 src/server/api/endpoints/posts/polls/vote.ts create mode 100644 src/server/api/endpoints/posts/reactions.ts create mode 100644 src/server/api/endpoints/posts/reactions/create.ts create mode 100644 src/server/api/endpoints/posts/reactions/delete.ts create mode 100644 src/server/api/endpoints/posts/replies.ts create mode 100644 src/server/api/endpoints/posts/reposts.ts create mode 100644 src/server/api/endpoints/posts/search.ts create mode 100644 src/server/api/endpoints/posts/show.ts create mode 100644 src/server/api/endpoints/posts/timeline.ts create mode 100644 src/server/api/endpoints/posts/trend.ts create mode 100644 src/server/api/endpoints/stats.ts create mode 100644 src/server/api/endpoints/sw/register.ts create mode 100644 src/server/api/endpoints/username/available.ts create mode 100644 src/server/api/endpoints/users.ts create mode 100644 src/server/api/endpoints/users/followers.ts create mode 100644 src/server/api/endpoints/users/following.ts create mode 100644 src/server/api/endpoints/users/get_frequently_replied_users.ts create mode 100644 src/server/api/endpoints/users/posts.ts create mode 100644 src/server/api/endpoints/users/recommendation.ts create mode 100644 src/server/api/endpoints/users/search.ts create mode 100644 src/server/api/endpoints/users/search_by_username.ts create mode 100644 src/server/api/endpoints/users/show.ts create mode 100644 src/server/api/event.ts create mode 100644 src/server/api/limitter.ts create mode 100644 src/server/api/models/access-token.ts create mode 100644 src/server/api/models/app.ts create mode 100644 src/server/api/models/appdata.ts create mode 100644 src/server/api/models/auth-session.ts create mode 100644 src/server/api/models/channel-watching.ts create mode 100644 src/server/api/models/channel.ts create mode 100644 src/server/api/models/drive-file.ts create mode 100644 src/server/api/models/drive-folder.ts create mode 100644 src/server/api/models/drive-tag.ts create mode 100644 src/server/api/models/favorite.ts create mode 100644 src/server/api/models/following.ts create mode 100644 src/server/api/models/messaging-history.ts create mode 100644 src/server/api/models/messaging-message.ts create mode 100644 src/server/api/models/meta.ts create mode 100644 src/server/api/models/mute.ts create mode 100644 src/server/api/models/notification.ts create mode 100644 src/server/api/models/othello-game.ts create mode 100644 src/server/api/models/othello-matching.ts create mode 100644 src/server/api/models/poll-vote.ts create mode 100644 src/server/api/models/post-reaction.ts create mode 100644 src/server/api/models/post-watching.ts create mode 100644 src/server/api/models/post.ts create mode 100644 src/server/api/models/signin.ts create mode 100644 src/server/api/models/sw-subscription.ts create mode 100644 src/server/api/models/user.ts create mode 100644 src/server/api/private/signin.ts create mode 100644 src/server/api/private/signup.ts create mode 100644 src/server/api/reply.ts create mode 100644 src/server/api/server.ts create mode 100644 src/server/api/service/github.ts create mode 100644 src/server/api/service/twitter.ts create mode 100644 src/server/api/stream/channel.ts create mode 100644 src/server/api/stream/drive.ts create mode 100644 src/server/api/stream/home.ts create mode 100644 src/server/api/stream/messaging-index.ts create mode 100644 src/server/api/stream/messaging.ts create mode 100644 src/server/api/stream/othello-game.ts create mode 100644 src/server/api/stream/othello.ts create mode 100644 src/server/api/stream/requests.ts create mode 100644 src/server/api/stream/server.ts create mode 100644 src/server/api/streaming.ts (limited to 'src/server/api') diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts new file mode 100644 index 0000000000..fb603a0e2a --- /dev/null +++ b/src/server/api/api-handler.ts @@ -0,0 +1,56 @@ +import * as express from 'express'; + +import { Endpoint } from './endpoints'; +import authenticate from './authenticate'; +import { IAuthContext } from './authenticate'; +import _reply from './reply'; +import limitter from './limitter'; + +export default async (endpoint: Endpoint, req: express.Request, res: express.Response) => { + const reply = _reply.bind(null, res); + let ctx: IAuthContext; + + // Authentication + try { + ctx = await authenticate(req); + } catch (e) { + return reply(403, 'AUTHENTICATION_FAILED'); + } + + if (endpoint.secure && !ctx.isSecure) { + return reply(403, 'ACCESS_DENIED'); + } + + if (endpoint.withCredential && ctx.user == null) { + return reply(401, 'PLZ_SIGNIN'); + } + + if (ctx.app && endpoint.kind) { + if (!ctx.app.permission.some(p => p === endpoint.kind)) { + return reply(403, 'ACCESS_DENIED'); + } + } + + if (endpoint.withCredential && endpoint.limit) { + try { + await limitter(endpoint, ctx); // Rate limit + } catch (e) { + // drop request if limit exceeded + return reply(429); + } + } + + let exec = require(`${__dirname}/endpoints/${endpoint.name}`); + + if (endpoint.withFile) { + exec = exec.bind(null, req.file); + } + + // API invoking + try { + const res = await exec(req.body, ctx.user, ctx.app, ctx.isSecure); + reply(res); + } catch (e) { + reply(400, e); + } +}; diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts new file mode 100644 index 0000000000..537c3d1e1f --- /dev/null +++ b/src/server/api/authenticate.ts @@ -0,0 +1,69 @@ +import * as express from 'express'; +import App from './models/app'; +import { default as User, IUser } from './models/user'; +import AccessToken from './models/access-token'; +import isNativeToken from './common/is-native-token'; + +export interface IAuthContext { + /** + * App which requested + */ + app: any; + + /** + * Authenticated user + */ + user: IUser; + + /** + * Whether requested with a User-Native Token + */ + isSecure: boolean; +} + +export default (req: express.Request) => new Promise(async (resolve, reject) => { + const token = req.body['i'] as string; + + if (token == null) { + return resolve({ + app: null, + user: null, + isSecure: false + }); + } + + if (isNativeToken(token)) { + const user: IUser = await User + .findOne({ 'account.token': token }); + + if (user === null) { + return reject('user not found'); + } + + return resolve({ + app: null, + user: user, + isSecure: true + }); + } else { + const accessToken = await AccessToken.findOne({ + hash: token.toLowerCase() + }); + + if (accessToken === null) { + return reject('invalid signature'); + } + + const app = await App + .findOne({ _id: accessToken.app_id }); + + const user = await User + .findOne({ _id: accessToken.user_id }); + + return resolve({ + app: app, + user: user, + isSecure: false + }); + } +}); diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts new file mode 100644 index 0000000000..77a68aaee6 --- /dev/null +++ b/src/server/api/bot/core.ts @@ -0,0 +1,438 @@ +import * as EventEmitter from 'events'; +import * as bcrypt from 'bcryptjs'; + +import User, { ILocalAccount, IUser, init as initUser } from '../models/user'; + +import getPostSummary from '../../common/get-post-summary'; +import getUserSummary from '../../common/user/get-summary'; +import parseAcct from '../../common/user/parse-acct'; +import getNotificationSummary from '../../common/get-notification-summary'; + +const hmm = [ + '?', + 'ふぅ~む...?', + 'ちょっと何言ってるかわからないです', + '「ヘルプ」と言うと利用可能な操作が確認できますよ' +]; + +/** + * Botの頭脳 + */ +export default class BotCore extends EventEmitter { + public user: IUser = null; + + private context: Context = null; + + constructor(user?: IUser) { + super(); + + this.user = user; + } + + public clearContext() { + this.setContext(null); + } + + public setContext(context: Context) { + this.context = context; + this.emit('updated'); + + if (context) { + context.on('updated', () => { + this.emit('updated'); + }); + } + } + + public export() { + return { + user: this.user, + context: this.context ? this.context.export() : null + }; + } + + protected _import(data) { + this.user = data.user ? initUser(data.user) : null; + this.setContext(data.context ? Context.import(this, data.context) : null); + } + + public static import(data) { + const bot = new BotCore(); + bot._import(data); + return bot; + } + + public async q(query: string): Promise { + if (this.context != null) { + return await this.context.q(query); + } + + if (/^@[a-zA-Z0-9-]+$/.test(query)) { + return await this.showUserCommand(query); + } + + switch (query) { + case 'ping': + return 'PONG'; + + case 'help': + case 'ヘルプ': + return '利用可能なコマンド一覧です:\n' + + 'help: これです\n' + + 'me: アカウント情報を見ます\n' + + 'login, signin: サインインします\n' + + 'logout, signout: サインアウトします\n' + + 'post: 投稿します\n' + + 'tl: タイムラインを見ます\n' + + 'no: 通知を見ます\n' + + '@<ユーザー名>: ユーザーを表示します\n' + + '\n' + + 'タイムラインや通知を見た後、「次」というとさらに遡ることができます。'; + + case 'me': + return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません'; + + case 'login': + case 'signin': + case 'ログイン': + case 'サインイン': + if (this.user != null) return '既にサインインしていますよ!'; + this.setContext(new SigninContext(this)); + return await this.context.greet(); + + case 'logout': + case 'signout': + case 'ログアウト': + case 'サインアウト': + if (this.user == null) return '今はサインインしてないですよ!'; + this.signout(); + return 'ご利用ありがとうございました <3'; + + case 'post': + case '投稿': + if (this.user == null) return 'まずサインインしてください。'; + this.setContext(new PostContext(this)); + return await this.context.greet(); + + case 'tl': + case 'タイムライン': + if (this.user == null) return 'まずサインインしてください。'; + this.setContext(new TlContext(this)); + return await this.context.greet(); + + case 'no': + case 'notifications': + case '通知': + if (this.user == null) return 'まずサインインしてください。'; + this.setContext(new NotificationsContext(this)); + return await this.context.greet(); + + case 'guessing-game': + case '数当てゲーム': + this.setContext(new GuessingGameContext(this)); + return await this.context.greet(); + + default: + return hmm[Math.floor(Math.random() * hmm.length)]; + } + } + + public signin(user: IUser) { + this.user = user; + this.emit('signin', user); + this.emit('updated'); + } + + public signout() { + const user = this.user; + this.user = null; + this.emit('signout', user); + this.emit('updated'); + } + + public async refreshUser() { + this.user = await User.findOne({ + _id: this.user._id + }, { + fields: { + data: false + } + }); + + this.emit('updated'); + } + + public async showUserCommand(q: string): Promise { + try { + const user = await require('../endpoints/users/show')(parseAcct(q.substr(1)), this.user); + + const text = getUserSummary(user); + + return text; + } catch (e) { + return `問題が発生したようです...: ${e}`; + } + } +} + +abstract class Context extends EventEmitter { + protected bot: BotCore; + + public abstract async greet(): Promise; + public abstract async q(query: string): Promise; + public abstract export(): any; + + constructor(bot: BotCore) { + super(); + this.bot = bot; + } + + public static import(bot: BotCore, data: any) { + if (data.type == 'guessing-game') return GuessingGameContext.import(bot, data.content); + if (data.type == 'post') return PostContext.import(bot, data.content); + if (data.type == 'tl') return TlContext.import(bot, data.content); + if (data.type == 'notifications') return NotificationsContext.import(bot, data.content); + if (data.type == 'signin') return SigninContext.import(bot, data.content); + return null; + } +} + +class SigninContext extends Context { + private temporaryUser: IUser = null; + + public async greet(): Promise { + return 'まずユーザー名を教えてください:'; + } + + public async q(query: string): Promise { + if (this.temporaryUser == null) { + // Fetch user + const user: IUser = await User.findOne({ + username_lower: query.toLowerCase(), + host: null + }, { + fields: { + data: false + } + }); + + if (user === null) { + return `${query}というユーザーは存在しませんでした... もう一度教えてください:`; + } else { + this.temporaryUser = user; + this.emit('updated'); + return `パスワードを教えてください:`; + } + } else { + // Compare password + const same = await bcrypt.compare(query, (this.temporaryUser.account as ILocalAccount).password); + + if (same) { + this.bot.signin(this.temporaryUser); + this.bot.clearContext(); + return `${this.temporaryUser.name}さん、おかえりなさい!`; + } else { + return `パスワードが違います... もう一度教えてください:`; + } + } + } + + public export() { + return { + type: 'signin', + content: { + temporaryUser: this.temporaryUser + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new SigninContext(bot); + context.temporaryUser = data.temporaryUser; + return context; + } +} + +class PostContext extends Context { + public async greet(): Promise { + return '内容:'; + } + + public async q(query: string): Promise { + await require('../endpoints/posts/create')({ + text: query + }, this.bot.user); + this.bot.clearContext(); + return '投稿しましたよ!'; + } + + public export() { + return { + type: 'post' + }; + } + + public static import(bot: BotCore, data: any) { + const context = new PostContext(bot); + return context; + } +} + +class TlContext extends Context { + private next: string = null; + + public async greet(): Promise { + return await this.getTl(); + } + + public async q(query: string): Promise { + if (query == '次') { + return await this.getTl(); + } else { + this.bot.clearContext(); + return await this.bot.q(query); + } + } + + private async getTl() { + const tl = await require('../endpoints/posts/timeline')({ + limit: 5, + until_id: this.next ? this.next : undefined + }, this.bot.user); + + if (tl.length > 0) { + this.next = tl[tl.length - 1].id; + this.emit('updated'); + + const text = tl + .map(post => `${post.user.name}\n「${getPostSummary(post)}」`) + .join('\n-----\n'); + + return text; + } else { + return 'タイムラインに表示するものがありません...'; + } + } + + public export() { + return { + type: 'tl', + content: { + next: this.next, + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new TlContext(bot); + context.next = data.next; + return context; + } +} + +class NotificationsContext extends Context { + private next: string = null; + + public async greet(): Promise { + return await this.getNotifications(); + } + + public async q(query: string): Promise { + if (query == '次') { + return await this.getNotifications(); + } else { + this.bot.clearContext(); + return await this.bot.q(query); + } + } + + private async getNotifications() { + const notifications = await require('../endpoints/i/notifications')({ + limit: 5, + until_id: this.next ? this.next : undefined + }, this.bot.user); + + if (notifications.length > 0) { + this.next = notifications[notifications.length - 1].id; + this.emit('updated'); + + const text = notifications + .map(notification => getNotificationSummary(notification)) + .join('\n-----\n'); + + return text; + } else { + return '通知はありません'; + } + } + + public export() { + return { + type: 'notifications', + content: { + next: this.next, + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new NotificationsContext(bot); + context.next = data.next; + return context; + } +} + +class GuessingGameContext extends Context { + private secret: number; + private history: number[] = []; + + public async greet(): Promise { + this.secret = Math.floor(Math.random() * 100); + this.emit('updated'); + return '0~100の秘密の数を当ててみてください:'; + } + + public async q(query: string): Promise { + if (query == 'やめる') { + this.bot.clearContext(); + return 'やめました。'; + } + + const guess = parseInt(query, 10); + + if (isNaN(guess)) { + return '整数で推測してください。「やめる」と言うとゲームをやめます。'; + } + + const firsttime = this.history.indexOf(guess) === -1; + + this.history.push(guess); + this.emit('updated'); + + if (this.secret < guess) { + return firsttime ? `${guess}よりも小さいですね` : `もう一度言いますが${guess}より小さいですよ`; + } else if (this.secret > guess) { + return firsttime ? `${guess}よりも大きいですね` : `もう一度言いますが${guess}より大きいですよ`; + } else { + this.bot.clearContext(); + return `正解です🎉 (${this.history.length}回目で当てました)`; + } + } + + public export() { + return { + type: 'guessing-game', + content: { + secret: this.secret, + history: this.history + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new GuessingGameContext(bot); + context.secret = data.secret; + context.history = data.history; + return context; + } +} diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts new file mode 100644 index 0000000000..5b3e9107f6 --- /dev/null +++ b/src/server/api/bot/interfaces/line.ts @@ -0,0 +1,238 @@ +import * as EventEmitter from 'events'; +import * as express from 'express'; +import * as request from 'request'; +import * as crypto from 'crypto'; +import User from '../../models/user'; +import config from '../../../../conf'; +import BotCore from '../core'; +import _redis from '../../../../db/redis'; +import prominence = require('prominence'); +import getAcct from '../../../common/user/get-acct'; +import parseAcct from '../../../common/user/parse-acct'; +import getPostSummary from '../../../common/get-post-summary'; + +const redis = prominence(_redis); + +// SEE: https://developers.line.me/media/messaging-api/messages/sticker_list.pdf +const stickers = [ + '297', + '298', + '299', + '300', + '301', + '302', + '303', + '304', + '305', + '306', + '307' +]; + +class LineBot extends BotCore { + private replyToken: string; + + private reply(messages: any[]) { + request.post({ + url: 'https://api.line.me/v2/bot/message/reply', + headers: { + 'Authorization': `Bearer ${config.line_bot.channel_access_token}` + }, + json: { + replyToken: this.replyToken, + messages: messages + } + }, (err, res, body) => { + if (err) { + console.error(err); + return; + } + }); + } + + public async react(ev: any): Promise { + this.replyToken = ev.replyToken; + + switch (ev.type) { + // メッセージ + case 'message': + switch (ev.message.type) { + // テキスト + case 'text': + const res = await this.q(ev.message.text); + if (res == null) return; + // 返信 + this.reply([{ + type: 'text', + text: res + }]); + break; + + // スタンプ + case 'sticker': + // スタンプで返信 + this.reply([{ + type: 'sticker', + packageId: '4', + stickerId: stickers[Math.floor(Math.random() * stickers.length)] + }]); + break; + } + break; + + // postback + case 'postback': + const data = ev.postback.data; + const cmd = data.split('|')[0]; + const arg = data.split('|')[1]; + switch (cmd) { + case 'showtl': + this.showUserTimelinePostback(arg); + break; + } + break; + } + } + + public static import(data) { + const bot = new LineBot(); + bot._import(data); + return bot; + } + + public async showUserCommand(q: string) { + const user = await require('../../endpoints/users/show')(parseAcct(q.substr(1)), this.user); + + const acct = getAcct(user); + const actions = []; + + actions.push({ + type: 'postback', + label: 'タイムラインを見る', + data: `showtl|${user.id}` + }); + + if (user.account.twitter) { + actions.push({ + type: 'uri', + label: 'Twitterアカウントを見る', + uri: `https://twitter.com/${user.account.twitter.screen_name}` + }); + } + + actions.push({ + type: 'uri', + label: 'Webで見る', + uri: `${config.url}/@${acct}` + }); + + this.reply([{ + type: 'template', + altText: await super.showUserCommand(q), + template: { + type: 'buttons', + thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`, + title: `${user.name} (@${acct})`, + text: user.description || '(no description)', + actions: actions + } + }]); + + return null; + } + + public async showUserTimelinePostback(userId: string) { + const tl = await require('../../endpoints/users/posts')({ + user_id: userId, + limit: 5 + }, this.user); + + const text = `${tl[0].user.name}さんのタイムラインはこちらです:\n\n` + tl + .map(post => getPostSummary(post)) + .join('\n-----\n'); + + this.reply([{ + type: 'text', + text: text + }]); + } +} + +module.exports = async (app: express.Application) => { + if (config.line_bot == null) return; + + const handler = new EventEmitter(); + + handler.on('event', async (ev) => { + + const sourceId = ev.source.userId; + const sessionId = `line-bot-sessions:${sourceId}`; + + const session = await redis.get(sessionId); + let bot: LineBot; + + if (session == null) { + const user = await User.findOne({ + host: null, + 'account.line': { + user_id: sourceId + } + }); + + bot = new LineBot(user); + + bot.on('signin', user => { + User.update(user._id, { + $set: { + 'account.line': { + user_id: sourceId + } + } + }); + }); + + bot.on('signout', user => { + User.update(user._id, { + $set: { + 'account.line': { + user_id: null + } + } + }); + }); + + redis.set(sessionId, JSON.stringify(bot.export())); + } else { + bot = LineBot.import(JSON.parse(session)); + } + + bot.on('updated', () => { + redis.set(sessionId, JSON.stringify(bot.export())); + }); + + if (session != null) bot.refreshUser(); + + bot.react(ev); + }); + + app.post('/hooks/line', (req, res, next) => { + // req.headers['x-line-signature'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + const sig1 = req.headers['x-line-signature'] as string; + + const hash = crypto.createHmac('SHA256', config.line_bot.channel_secret) + .update((req as any).rawBody); + + const sig2 = hash.digest('base64'); + + // シグネチャ比較 + if (sig1 === sig2) { + req.body.events.forEach(ev => { + handler.emit('event', ev); + }); + + res.sendStatus(200); + } else { + res.sendStatus(400); + } + }); +}; diff --git a/src/server/api/common/drive/add-file.ts b/src/server/api/common/drive/add-file.ts new file mode 100644 index 0000000000..5f3c69c15a --- /dev/null +++ b/src/server/api/common/drive/add-file.ts @@ -0,0 +1,307 @@ +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, { getGridFSBucket } from '../../models/drive-file'; +import DriveFolder from '../../models/drive-folder'; +import { pack } from '../../models/drive-file'; +import event, { publishDriveStream } from '../../event'; +import getAcct from '../../../common/user/get-acct'; +import config from '../../../../conf'; + +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 +) => { + 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.user_id': 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, + user_id: user._id + }); + if (!driveFolder) { + throw 'folder-not-found'; + } + return driveFolder; + })(), + // usage checker + (async () => { + // Calculate drive usage + const usage = await DriveFile + .aggregate([{ + $match: { 'metadata.user_id': 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.drive_capacity) { + throw 'no-free-space'; + } + })() + ]); + + const readable = fs.createReadStream(path); + + const properties = {}; + + if (wh) { + properties['width'] = wh[0]; + properties['height'] = wh[1]; + } + + if (averageColor) { + properties['average_color'] = averageColor; + } + + return addToGridFS(detectedName, readable, mime, { + user_id: user._id, + folder_id: folder !== null ? folder._id : null, + comment: comment, + properties: properties + }); +}; + +/** + * 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, + user_id: user._id.toString() + } + }); + } + }); + }) + .catch(reject); +}); diff --git a/src/server/api/common/drive/upload_from_url.ts b/src/server/api/common/drive/upload_from_url.ts new file mode 100644 index 0000000000..5dd9695936 --- /dev/null +++ b/src/server/api/common/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): 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); + + // clean-up + fs.unlink(path, (e) => { + if (e) log(e.stack); + }); + + return driveFile; +}; diff --git a/src/server/api/common/generate-native-user-token.ts b/src/server/api/common/generate-native-user-token.ts new file mode 100644 index 0000000000..2082b89a5a --- /dev/null +++ b/src/server/api/common/generate-native-user-token.ts @@ -0,0 +1,3 @@ +import rndstr from 'rndstr'; + +export default () => `!${rndstr('a-zA-Z0-9', 32)}`; diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts new file mode 100644 index 0000000000..db6313816d --- /dev/null +++ b/src/server/api/common/get-friends.ts @@ -0,0 +1,26 @@ +import * as mongodb from 'mongodb'; +import Following from '../models/following'; + +export default async (me: mongodb.ObjectID, includeMe: boolean = true) => { + // Fetch relation to other users who the I follows + // SELECT followee + const myfollowing = await Following + .find({ + follower_id: me, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }, { + fields: { + followee_id: true + } + }); + + // ID list of other users who the I follows + const myfollowingIds = myfollowing.map(follow => follow.followee_id); + + if (includeMe) { + myfollowingIds.push(me); + } + + return myfollowingIds; +}; diff --git a/src/server/api/common/get-host-lower.ts b/src/server/api/common/get-host-lower.ts new file mode 100644 index 0000000000..fc4b30439e --- /dev/null +++ b/src/server/api/common/get-host-lower.ts @@ -0,0 +1,5 @@ +import { toUnicode } from 'punycode'; + +export default host => { + return toUnicode(host).replace(/[A-Z]+/, match => match.toLowerCase()); +}; diff --git a/src/server/api/common/is-native-token.ts b/src/server/api/common/is-native-token.ts new file mode 100644 index 0000000000..0769a4812e --- /dev/null +++ b/src/server/api/common/is-native-token.ts @@ -0,0 +1 @@ +export default (token: string) => token[0] == '!'; diff --git a/src/server/api/common/notify.ts b/src/server/api/common/notify.ts new file mode 100644 index 0000000000..ae5669b84c --- /dev/null +++ b/src/server/api/common/notify.ts @@ -0,0 +1,50 @@ +import * as mongo from 'mongodb'; +import Notification from '../models/notification'; +import Mute from '../models/mute'; +import event from '../event'; +import { pack } from '../models/notification'; + +export default ( + notifiee: mongo.ObjectID, + notifier: mongo.ObjectID, + type: string, + content?: any +) => new Promise(async (resolve, reject) => { + if (notifiee.equals(notifier)) { + return resolve(); + } + + // Create notification + const notification = await Notification.insert(Object.assign({ + created_at: new Date(), + notifiee_id: notifiee, + notifier_id: notifier, + type: type, + is_read: false + }, content)); + + resolve(notification); + + // Publish notification event + event(notifiee, 'notification', + await pack(notification)); + + // 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する + setTimeout(async () => { + const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true }); + if (!fresh.is_read) { + //#region ただしミュートしているユーザーからの通知なら無視 + const mute = await Mute.find({ + muter_id: notifiee, + deleted_at: { $exists: false } + }); + const mutedUserIds = mute.map(m => m.mutee_id.toString()); + if (mutedUserIds.indexOf(notifier.toString()) != -1) { + return; + } + //#endregion + + event(notifiee, 'unread_notification', await pack(notification)); + } + }, 3000); +}); diff --git a/src/server/api/common/push-sw.ts b/src/server/api/common/push-sw.ts new file mode 100644 index 0000000000..b33715eb18 --- /dev/null +++ b/src/server/api/common/push-sw.ts @@ -0,0 +1,52 @@ +const push = require('web-push'); +import * as mongo from 'mongodb'; +import Subscription from '../models/sw-subscription'; +import config from '../../../conf'; + +if (config.sw) { + // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 + push.setVapidDetails( + config.maintainer.url, + config.sw.public_key, + config.sw.private_key); +} + +export default async function(userId: mongo.ObjectID | string, type, body?) { + if (!config.sw) return; + + if (typeof userId === 'string') { + userId = new mongo.ObjectID(userId); + } + + // Fetch + const subscriptions = await Subscription.find({ + user_id: userId + }); + + subscriptions.forEach(subscription => { + const pushSubscription = { + endpoint: subscription.endpoint, + keys: { + auth: subscription.auth, + p256dh: subscription.publickey + } + }; + + push.sendNotification(pushSubscription, JSON.stringify({ + type, body + })).catch(err => { + //console.log(err.statusCode); + //console.log(err.headers); + //console.log(err.body); + + if (err.statusCode == 410) { + Subscription.remove({ + user_id: userId, + endpoint: subscription.endpoint, + auth: subscription.auth, + publickey: subscription.publickey + }); + } + }); + }); +} diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts new file mode 100644 index 0000000000..8e5e5b2b68 --- /dev/null +++ b/src/server/api/common/read-messaging-message.ts @@ -0,0 +1,66 @@ +import * as mongo from 'mongodb'; +import Message from '../models/messaging-message'; +import { IMessagingMessage as IMessage } from '../models/messaging-message'; +import publishUserStream from '../event'; +import { publishMessagingStream } from '../event'; +import { publishMessagingIndexStream } from '../event'; + +/** + * Mark as read message(s) + */ +export default ( + user: string | mongo.ObjectID, + otherparty: string | mongo.ObjectID, + message: string | string[] | IMessage | IMessage[] | mongo.ObjectID | mongo.ObjectID[] +) => new Promise(async (resolve, reject) => { + + const userId = mongo.ObjectID.prototype.isPrototypeOf(user) + ? user + : new mongo.ObjectID(user); + + const otherpartyId = mongo.ObjectID.prototype.isPrototypeOf(otherparty) + ? otherparty + : new mongo.ObjectID(otherparty); + + const ids: mongo.ObjectID[] = Array.isArray(message) + ? mongo.ObjectID.prototype.isPrototypeOf(message[0]) + ? (message as mongo.ObjectID[]) + : typeof message[0] === 'string' + ? (message as string[]).map(m => new mongo.ObjectID(m)) + : (message as IMessage[]).map(m => m._id) + : mongo.ObjectID.prototype.isPrototypeOf(message) + ? [(message as mongo.ObjectID)] + : typeof message === 'string' + ? [new mongo.ObjectID(message)] + : [(message as IMessage)._id]; + + // Update documents + await Message.update({ + _id: { $in: ids }, + user_id: otherpartyId, + recipient_id: userId, + is_read: false + }, { + $set: { + is_read: true + } + }, { + multi: true + }); + + // Publish event + publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString())); + publishMessagingIndexStream(userId, 'read', ids.map(id => id.toString())); + + // Calc count of my unread messages + const count = await Message + .count({ + recipient_id: userId, + is_read: false + }); + + if (count == 0) { + // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 + publishUserStream(userId, 'read_all_messaging_messages'); + } +}); diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts new file mode 100644 index 0000000000..3009cc5d08 --- /dev/null +++ b/src/server/api/common/read-notification.ts @@ -0,0 +1,52 @@ +import * as mongo from 'mongodb'; +import { default as Notification, INotification } from '../models/notification'; +import publishUserStream from '../event'; + +/** + * Mark as read notification(s) + */ +export default ( + user: string | mongo.ObjectID, + message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[] +) => new Promise(async (resolve, reject) => { + + const userId = mongo.ObjectID.prototype.isPrototypeOf(user) + ? user + : new mongo.ObjectID(user); + + const ids: mongo.ObjectID[] = Array.isArray(message) + ? mongo.ObjectID.prototype.isPrototypeOf(message[0]) + ? (message as mongo.ObjectID[]) + : typeof message[0] === 'string' + ? (message as string[]).map(m => new mongo.ObjectID(m)) + : (message as INotification[]).map(m => m._id) + : mongo.ObjectID.prototype.isPrototypeOf(message) + ? [(message as mongo.ObjectID)] + : typeof message === 'string' + ? [new mongo.ObjectID(message)] + : [(message as INotification)._id]; + + // Update documents + await Notification.update({ + _id: { $in: ids }, + is_read: false + }, { + $set: { + is_read: true + } + }, { + multi: true + }); + + // Calc count of my unread notifications + const count = await Notification + .count({ + notifiee_id: userId, + is_read: false + }); + + if (count == 0) { + // 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行 + publishUserStream(userId, 'read_all_notifications'); + } +}); diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts new file mode 100644 index 0000000000..a11ea56c0c --- /dev/null +++ b/src/server/api/common/signin.ts @@ -0,0 +1,19 @@ +import config from '../../../conf'; + +export default function(res, user, redirect: boolean) { + const expires = 1000 * 60 * 60 * 24 * 365; // One Year + res.cookie('i', user.account.token, { + path: '/', + domain: `.${config.hostname}`, + secure: config.url.substr(0, 5) === 'https', + httpOnly: false, + expires: new Date(Date.now() + expires), + maxAge: expires + }); + + if (redirect) { + res.redirect(config.url); + } else { + res.sendStatus(204); + } +} diff --git a/src/server/api/common/text/core/syntax-highlighter.ts b/src/server/api/common/text/core/syntax-highlighter.ts new file mode 100644 index 0000000000..c0396b1fc6 --- /dev/null +++ b/src/server/api/common/text/core/syntax-highlighter.ts @@ -0,0 +1,334 @@ +function escape(text) { + return text + .replace(/>/g, '>') + .replace(/ k[0].toUpperCase() + k.substr(1))) + .concat(_keywords.map(k => k.toUpperCase())) + .sort((a, b) => b.length - a.length); + +const symbols = [ + '=', + '+', + '-', + '*', + '/', + '%', + '~', + '^', + '&', + '|', + '>', + '<', + '!', + '?' +]; + +const elements = [ + // comment + code => { + if (code.substr(0, 2) != '//') return null; + const match = code.match(/^\/\/(.+?)(\n|$)/); + if (!match) return null; + const comment = match[0]; + return { + html: `${escape(comment)}`, + next: comment.length + }; + }, + + // block comment + code => { + const match = code.match(/^\/\*([\s\S]+?)\*\//); + if (!match) return null; + return { + html: `${escape(match[0])}`, + next: match[0].length + }; + }, + + // string + code => { + if (!/^['"`]/.test(code)) return null; + const begin = code[0]; + let str = begin; + let thisIsNotAString = false; + for (let i = 1; i < code.length; i++) { + const char = code[i]; + if (char == '\\') { + str += char; + str += code[i + 1] || ''; + i++; + continue; + } else if (char == begin) { + str += char; + break; + } else if (char == '\n' || i == (code.length - 1)) { + thisIsNotAString = true; + break; + } else { + str += char; + } + } + if (thisIsNotAString) { + return null; + } else { + return { + html: `${escape(str)}`, + next: str.length + }; + } + }, + + // regexp + code => { + if (code[0] != '/') return null; + let regexp = ''; + let thisIsNotARegexp = false; + for (let i = 1; i < code.length; i++) { + const char = code[i]; + if (char == '\\') { + regexp += char; + regexp += code[i + 1] || ''; + i++; + continue; + } else if (char == '/') { + break; + } else if (char == '\n' || i == (code.length - 1)) { + thisIsNotARegexp = true; + break; + } else { + regexp += char; + } + } + + if (thisIsNotARegexp) return null; + if (regexp == '') return null; + if (regexp[0] == ' ' && regexp[regexp.length - 1] == ' ') return null; + + return { + html: `/${escape(regexp)}/`, + next: regexp.length + 2 + }; + }, + + // label + code => { + if (code[0] != '@') return null; + const match = code.match(/^@([a-zA-Z_-]+?)\n/); + if (!match) return null; + const label = match[0]; + return { + html: `${label}`, + next: label.length + }; + }, + + // number + (code, i, source) => { + const prev = source[i - 1]; + if (prev && /[a-zA-Z]/.test(prev)) return null; + if (!/^[\-\+]?[0-9\.]+/.test(code)) return null; + const match = code.match(/^[\-\+]?[0-9\.]+/)[0]; + if (match) { + return { + html: `${match}`, + next: match.length + }; + } else { + return null; + } + }, + + // nan + (code, i, source) => { + const prev = source[i - 1]; + if (prev && /[a-zA-Z]/.test(prev)) return null; + if (code.substr(0, 3) == 'NaN') { + return { + html: `NaN`, + next: 3 + }; + } else { + return null; + } + }, + + // method + code => { + const match = code.match(/^([a-zA-Z_-]+?)\(/); + if (!match) return null; + + if (match[1] == '-') return null; + + return { + html: `${match[1]}`, + next: match[1].length + }; + }, + + // property + (code, i, source) => { + const prev = source[i - 1]; + if (prev != '.') return null; + + const match = code.match(/^[a-zA-Z0-9_-]+/); + if (!match) return null; + + return { + html: `${match[0]}`, + next: match[0].length + }; + }, + + // keyword + (code, i, source) => { + const prev = source[i - 1]; + if (prev && /[a-zA-Z]/.test(prev)) return null; + + const match = keywords.filter(k => code.substr(0, k.length) == k)[0]; + if (match) { + if (/^[a-zA-Z]/.test(code.substr(match.length))) return null; + return { + html: `${match}`, + next: match.length + }; + } else { + return null; + } + }, + + // symbol + code => { + const match = symbols.filter(s => code[0] == s)[0]; + if (match) { + return { + html: `${match}`, + next: 1 + }; + } else { + return null; + } + } +]; + +// specify lang is todo +export default (source: string, lang?: string) => { + let code = source; + let html = ''; + + let i = 0; + + function push(token) { + html += token.html; + code = code.substr(token.next); + i += token.next; + } + + while (code != '') { + const parsed = elements.some(el => { + const e = el(code, i, source); + if (e) { + push(e); + return true; + } else { + return false; + } + }); + + if (!parsed) { + push({ + html: escape(code[0]), + next: 1 + }); + } + } + + return html; +}; diff --git a/src/server/api/common/text/elements/bold.ts b/src/server/api/common/text/elements/bold.ts new file mode 100644 index 0000000000..ce25764457 --- /dev/null +++ b/src/server/api/common/text/elements/bold.ts @@ -0,0 +1,14 @@ +/** + * Bold + */ + +module.exports = text => { + const match = text.match(/^\*\*(.+?)\*\*/); + if (!match) return null; + const bold = match[0]; + return { + type: 'bold', + content: bold, + bold: bold.substr(2, bold.length - 4) + }; +}; diff --git a/src/server/api/common/text/elements/code.ts b/src/server/api/common/text/elements/code.ts new file mode 100644 index 0000000000..4821e95fe2 --- /dev/null +++ b/src/server/api/common/text/elements/code.ts @@ -0,0 +1,17 @@ +/** + * Code (block) + */ + +import genHtml from '../core/syntax-highlighter'; + +module.exports = text => { + const match = text.match(/^```([\s\S]+?)```/); + if (!match) return null; + const code = match[0]; + return { + type: 'code', + content: code, + code: code.substr(3, code.length - 6).trim(), + html: genHtml(code.substr(3, code.length - 6).trim()) + }; +}; diff --git a/src/server/api/common/text/elements/emoji.ts b/src/server/api/common/text/elements/emoji.ts new file mode 100644 index 0000000000..e24231a223 --- /dev/null +++ b/src/server/api/common/text/elements/emoji.ts @@ -0,0 +1,14 @@ +/** + * Emoji + */ + +module.exports = text => { + const match = text.match(/^:[a-zA-Z0-9+-_]+:/); + if (!match) return null; + const emoji = match[0]; + return { + type: 'emoji', + content: emoji, + emoji: emoji.substr(1, emoji.length - 2) + }; +}; diff --git a/src/server/api/common/text/elements/hashtag.ts b/src/server/api/common/text/elements/hashtag.ts new file mode 100644 index 0000000000..ee57b140b8 --- /dev/null +++ b/src/server/api/common/text/elements/hashtag.ts @@ -0,0 +1,19 @@ +/** + * Hashtag + */ + +module.exports = (text, i) => { + if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null; + const isHead = text[0] == '#'; + const hashtag = text.match(/^\s?#[^\s]+/)[0]; + const res: any[] = !isHead ? [{ + type: 'text', + content: text[0] + }] : []; + res.push({ + type: 'hashtag', + content: isHead ? hashtag : hashtag.substr(1), + hashtag: isHead ? hashtag.substr(1) : hashtag.substr(2) + }); + return res; +}; diff --git a/src/server/api/common/text/elements/inline-code.ts b/src/server/api/common/text/elements/inline-code.ts new file mode 100644 index 0000000000..9f9ef51a2b --- /dev/null +++ b/src/server/api/common/text/elements/inline-code.ts @@ -0,0 +1,17 @@ +/** + * Code (inline) + */ + +import genHtml from '../core/syntax-highlighter'; + +module.exports = text => { + const match = text.match(/^`(.+?)`/); + if (!match) return null; + const code = match[0]; + return { + type: 'inline-code', + content: code, + code: code.substr(1, code.length - 2).trim(), + html: genHtml(code.substr(1, code.length - 2).trim()) + }; +}; diff --git a/src/server/api/common/text/elements/link.ts b/src/server/api/common/text/elements/link.ts new file mode 100644 index 0000000000..35563ddc3d --- /dev/null +++ b/src/server/api/common/text/elements/link.ts @@ -0,0 +1,19 @@ +/** + * Link + */ + +module.exports = text => { + const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/); + if (!match) return null; + const silent = text[0] == '?'; + const link = match[0]; + const title = match[1]; + const url = match[2]; + return { + type: 'link', + content: link, + title: title, + url: url, + silent: silent + }; +}; diff --git a/src/server/api/common/text/elements/mention.ts b/src/server/api/common/text/elements/mention.ts new file mode 100644 index 0000000000..2025dfdaad --- /dev/null +++ b/src/server/api/common/text/elements/mention.ts @@ -0,0 +1,17 @@ +/** + * Mention + */ +import parseAcct from '../../../../common/user/parse-acct'; + +module.exports = text => { + const match = text.match(/^(?:@[a-zA-Z0-9\-]+){1,2}/); + if (!match) return null; + const mention = match[0]; + const { username, host } = parseAcct(mention.substr(1)); + return { + type: 'mention', + content: mention, + username, + host + }; +}; diff --git a/src/server/api/common/text/elements/quote.ts b/src/server/api/common/text/elements/quote.ts new file mode 100644 index 0000000000..cc8cfffdc4 --- /dev/null +++ b/src/server/api/common/text/elements/quote.ts @@ -0,0 +1,14 @@ +/** + * Quoted text + */ + +module.exports = text => { + const match = text.match(/^"([\s\S]+?)\n"/); + if (!match) return null; + const quote = match[0]; + return { + type: 'quote', + content: quote, + quote: quote.substr(1, quote.length - 2).trim(), + }; +}; diff --git a/src/server/api/common/text/elements/url.ts b/src/server/api/common/text/elements/url.ts new file mode 100644 index 0000000000..1003aff9c3 --- /dev/null +++ b/src/server/api/common/text/elements/url.ts @@ -0,0 +1,14 @@ +/** + * URL + */ + +module.exports = text => { + const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+/); + if (!match) return null; + const url = match[0]; + return { + type: 'url', + content: url, + url: url + }; +}; diff --git a/src/server/api/common/text/index.ts b/src/server/api/common/text/index.ts new file mode 100644 index 0000000000..1e2398dc38 --- /dev/null +++ b/src/server/api/common/text/index.ts @@ -0,0 +1,72 @@ +/** + * Misskey Text Analyzer + */ + +const elements = [ + require('./elements/bold'), + require('./elements/url'), + require('./elements/link'), + require('./elements/mention'), + require('./elements/hashtag'), + require('./elements/code'), + require('./elements/inline-code'), + require('./elements/quote'), + require('./elements/emoji') +]; + +export default (source: string) => { + + if (source == '') { + return null; + } + + const tokens = []; + + function push(token) { + if (token != null) { + tokens.push(token); + source = source.substr(token.content.length); + } + } + + let i = 0; + + // パース + while (source != '') { + const parsed = elements.some(el => { + let _tokens = el(source, i); + if (_tokens) { + if (!Array.isArray(_tokens)) { + _tokens = [_tokens]; + } + _tokens.forEach(push); + return true; + } else { + return false; + } + }); + + if (!parsed) { + push({ + type: 'text', + content: source[0] + }); + } + + i++; + } + + // テキストを纏める + tokens[0] = [tokens[0]]; + return tokens.reduce((a, b) => { + if (a[a.length - 1].type == 'text' && b.type == 'text') { + const tail = a.pop(); + return a.concat({ + type: 'text', + content: tail.content + b.content + }); + } else { + return a.concat(b); + } + }); +}; diff --git a/src/server/api/common/watch-post.ts b/src/server/api/common/watch-post.ts new file mode 100644 index 0000000000..1a50f0edaa --- /dev/null +++ b/src/server/api/common/watch-post.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).user_id)) { + return; + } + + // if watching now + const exist = await Watching.findOne({ + post_id: (post as any)._id, + user_id: me, + deleted_at: { $exists: false } + }); + + if (exist !== null) { + return; + } + + await Watching.insert({ + created_at: new Date(), + post_id: (post as any)._id, + user_id: me + }); +}; diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts new file mode 100644 index 0000000000..c7100bd036 --- /dev/null +++ b/src/server/api/endpoints.ts @@ -0,0 +1,584 @@ +const ms = require('ms'); + +/** + * エンドポイントを表します。 + */ +export type Endpoint = { + + /** + * エンドポイント名 + */ + name: string; + + /** + * このエンドポイントにリクエストするのにユーザー情報が必須か否か + * 省略した場合は false として解釈されます。 + */ + withCredential?: boolean; + + /** + * エンドポイントのリミテーションに関するやつ + * 省略した場合はリミテーションは無いものとして解釈されます。 + * また、withCredential が false の場合はリミテーションを行うことはできません。 + */ + limit?: { + + /** + * 複数のエンドポイントでリミットを共有したい場合に指定するキー + */ + key?: string; + + /** + * リミットを適用する期間(ms) + * このプロパティを設定する場合、max プロパティも設定する必要があります。 + */ + duration?: number; + + /** + * durationで指定した期間内にいくつまでリクエストできるのか + * このプロパティを設定する場合、duration プロパティも設定する必要があります。 + */ + max?: number; + + /** + * 最低でもどれくらいの間隔を開けてリクエストしなければならないか(ms) + */ + minInterval?: number; + }; + + /** + * ファイルの添付を必要とするか否か + * 省略した場合は false として解釈されます。 + */ + withFile?: boolean; + + /** + * サードパーティアプリからはリクエストすることができないか否か + * 省略した場合は false として解釈されます。 + */ + secure?: boolean; + + /** + * エンドポイントの種類 + * パーミッションの実現に利用されます。 + */ + kind?: string; +}; + +const endpoints: Endpoint[] = [ + { + name: 'meta' + }, + { + name: 'stats' + }, + { + name: 'username/available' + }, + { + name: 'my/apps', + withCredential: true + }, + { + name: 'app/create', + withCredential: true, + limit: { + duration: ms('1day'), + max: 3 + } + }, + { + name: 'app/show' + }, + { + name: 'app/name_id/available' + }, + { + name: 'auth/session/generate' + }, + { + name: 'auth/session/show' + }, + { + name: 'auth/session/userkey' + }, + { + name: 'auth/accept', + withCredential: true, + secure: true + }, + { + name: 'auth/deny', + withCredential: true, + secure: true + }, + { + name: 'aggregation/posts', + }, + { + name: 'aggregation/users', + }, + { + name: 'aggregation/users/activity', + }, + { + name: 'aggregation/users/post', + }, + { + name: 'aggregation/users/followers' + }, + { + name: 'aggregation/users/following' + }, + { + name: 'aggregation/users/reaction' + }, + { + name: 'aggregation/posts/repost' + }, + { + name: 'aggregation/posts/reply' + }, + { + name: 'aggregation/posts/reaction' + }, + { + name: 'aggregation/posts/reactions' + }, + + { + name: 'sw/register', + withCredential: true + }, + + { + name: 'i', + withCredential: true + }, + { + name: 'i/2fa/register', + withCredential: true, + secure: true + }, + { + name: 'i/2fa/unregister', + withCredential: true, + secure: true + }, + { + name: 'i/2fa/done', + withCredential: true, + secure: true + }, + { + name: 'i/update', + withCredential: true, + limit: { + duration: ms('1day'), + max: 50 + }, + kind: 'account-write' + }, + { + name: 'i/update_home', + withCredential: true, + secure: true + }, + { + name: 'i/update_mobile_home', + withCredential: true, + secure: true + }, + { + name: 'i/change_password', + withCredential: true, + secure: true + }, + { + name: 'i/regenerate_token', + withCredential: true, + secure: true + }, + { + name: 'i/update_client_setting', + withCredential: true, + secure: true + }, + { + name: 'i/pin', + kind: 'account-write' + }, + { + name: 'i/appdata/get', + withCredential: true + }, + { + name: 'i/appdata/set', + withCredential: true + }, + { + name: 'i/signin_history', + withCredential: true, + kind: 'account-read' + }, + { + name: 'i/authorized_apps', + withCredential: true, + secure: true + }, + + { + name: 'i/notifications', + withCredential: true, + kind: 'notification-read' + }, + + { + name: 'othello/match', + withCredential: true + }, + + { + name: 'othello/match/cancel', + withCredential: true + }, + + { + name: 'othello/invitations', + withCredential: true + }, + + { + name: 'othello/games', + withCredential: true + }, + + { + name: 'othello/games/show' + }, + + { + name: 'mute/create', + withCredential: true, + kind: 'account/write' + }, + { + name: 'mute/delete', + withCredential: true, + kind: 'account/write' + }, + { + name: 'mute/list', + withCredential: true, + kind: 'account/read' + }, + + { + name: 'notifications/get_unread_count', + withCredential: true, + kind: 'notification-read' + }, + { + name: 'notifications/delete', + withCredential: true, + kind: 'notification-write' + }, + { + name: 'notifications/delete_all', + withCredential: true, + kind: 'notification-write' + }, + { + name: 'notifications/mark_as_read_all', + withCredential: true, + kind: 'notification-write' + }, + + { + name: 'drive', + withCredential: true, + kind: 'drive-read' + }, + { + name: 'drive/stream', + withCredential: true, + kind: 'drive-read' + }, + { + name: 'drive/files', + withCredential: true, + kind: 'drive-read' + }, + { + name: 'drive/files/create', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 100 + }, + withFile: true, + kind: 'drive-write' + }, + { + name: 'drive/files/upload_from_url', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 10 + }, + kind: 'drive-write' + }, + { + name: 'drive/files/show', + withCredential: true, + kind: 'drive-read' + }, + { + name: 'drive/files/find', + withCredential: true, + kind: 'drive-read' + }, + { + name: 'drive/files/delete', + withCredential: true, + kind: 'drive-write' + }, + { + name: 'drive/files/update', + withCredential: true, + kind: 'drive-write' + }, + { + name: 'drive/folders', + withCredential: true, + kind: 'drive-read' + }, + { + name: 'drive/folders/create', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 50 + }, + kind: 'drive-write' + }, + { + name: 'drive/folders/show', + withCredential: true, + kind: 'drive-read' + }, + { + name: 'drive/folders/find', + withCredential: true, + kind: 'drive-read' + }, + { + name: 'drive/folders/update', + withCredential: true, + kind: 'drive-write' + }, + + { + name: 'users' + }, + { + name: 'users/show' + }, + { + name: 'users/search' + }, + { + name: 'users/search_by_username' + }, + { + name: 'users/posts' + }, + { + name: 'users/following' + }, + { + name: 'users/followers' + }, + { + name: 'users/recommendation', + withCredential: true, + kind: 'account-read' + }, + { + name: 'users/get_frequently_replied_users' + }, + + { + name: 'following/create', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 100 + }, + kind: 'following-write' + }, + { + name: 'following/delete', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 100 + }, + kind: 'following-write' + }, + + { + name: 'posts' + }, + { + name: 'posts/show' + }, + { + name: 'posts/replies' + }, + { + name: 'posts/context' + }, + { + name: 'posts/create', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 120, + minInterval: ms('1second') + }, + kind: 'post-write' + }, + { + name: 'posts/reposts' + }, + { + name: 'posts/search' + }, + { + name: 'posts/timeline', + withCredential: true, + limit: { + duration: ms('10minutes'), + max: 100 + } + }, + { + name: 'posts/mentions', + withCredential: true, + limit: { + duration: ms('10minutes'), + max: 100 + } + }, + { + name: 'posts/trend', + withCredential: true + }, + { + name: 'posts/categorize', + withCredential: true + }, + { + name: 'posts/reactions', + withCredential: true + }, + { + name: 'posts/reactions/create', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 100 + }, + kind: 'reaction-write' + }, + { + name: 'posts/reactions/delete', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 100 + }, + kind: 'reaction-write' + }, + { + name: 'posts/favorites/create', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 100 + }, + kind: 'favorite-write' + }, + { + name: 'posts/favorites/delete', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 100 + }, + kind: 'favorite-write' + }, + { + name: 'posts/polls/vote', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 100 + }, + kind: 'vote-write' + }, + { + name: 'posts/polls/recommendation', + withCredential: true + }, + + { + name: 'messaging/history', + withCredential: true, + kind: 'messaging-read' + }, + { + name: 'messaging/unread', + withCredential: true, + kind: 'messaging-read' + }, + { + name: 'messaging/messages', + withCredential: true, + kind: 'messaging-read' + }, + { + name: 'messaging/messages/create', + withCredential: true, + kind: 'messaging-write' + }, + { + name: 'channels/create', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 3, + minInterval: ms('10seconds') + } + }, + { + name: 'channels/show' + }, + { + name: 'channels/posts' + }, + { + name: 'channels/watch', + withCredential: true + }, + { + name: 'channels/unwatch', + withCredential: true + }, + { + name: 'channels' + }, +]; + +export default endpoints; diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts new file mode 100644 index 0000000000..9d8bccbdb2 --- /dev/null +++ b/src/server/api/endpoints/aggregation/posts.ts @@ -0,0 +1,90 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post from '../../models/post'; + +/** + * Aggregate posts + * + * @param {any} params + * @return {Promise} + */ +module.exports = params => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 365, limitErr] = $(params.limit).optional.number().range(1, 365).$; + if (limitErr) return rej('invalid limit param'); + + const datas = await Post + .aggregate([ + { $project: { + repost_id: '$repost_id', + reply_id: '$reply_id', + created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + }}, + { $project: { + date: { + year: { $year: '$created_at' }, + month: { $month: '$created_at' }, + day: { $dayOfMonth: '$created_at' } + }, + type: { + $cond: { + if: { $ne: ['$repost_id', null] }, + then: 'repost', + else: { + $cond: { + if: { $ne: ['$reply_id', null] }, + then: 'reply', + else: 'post' + } + } + } + }} + }, + { $group: { _id: { + date: '$date', + type: '$type' + }, count: { $sum: 1 } } }, + { $group: { + _id: '$_id.date', + data: { $addToSet: { + type: '$_id.type', + count: '$count' + }} + } } + ]); + + datas.forEach(data => { + data.date = data._id; + delete data._id; + + data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count; + data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count; + data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count; + + delete data.data; + }); + + const graph = []; + + for (let i = 0; i < limit; i++) { + const day = new Date(new Date().setDate(new Date().getDate() - i)); + + const data = datas.filter(d => + d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() + )[0]; + + if (data) { + graph.push(data); + } else { + graph.push({ + posts: 0, + reposts: 0, + replies: 0 + }); + } + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/posts/reaction.ts b/src/server/api/endpoints/aggregation/posts/reaction.ts new file mode 100644 index 0000000000..eb99b9d088 --- /dev/null +++ b/src/server/api/endpoints/aggregation/posts/reaction.ts @@ -0,0 +1,76 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post from '../../../models/post'; +import Reaction from '../../../models/post-reaction'; + +/** + * Aggregate reaction of a post + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Lookup post + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + const datas = await Reaction + .aggregate([ + { $match: { post_id: post._id } }, + { $project: { + created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + }}, + { $project: { + date: { + year: { $year: '$created_at' }, + month: { $month: '$created_at' }, + day: { $dayOfMonth: '$created_at' } + } + }}, + { $group: { + _id: '$date', + count: { $sum: 1 } + }} + ]); + + datas.forEach(data => { + data.date = data._id; + delete data._id; + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + const day = new Date(new Date().setDate(new Date().getDate() - i)); + + const data = datas.filter(d => + d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() + )[0]; + + if (data) { + graph.push(data); + } else { + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: 0 + }); + } + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/posts/reactions.ts b/src/server/api/endpoints/aggregation/posts/reactions.ts new file mode 100644 index 0000000000..790b523be9 --- /dev/null +++ b/src/server/api/endpoints/aggregation/posts/reactions.ts @@ -0,0 +1,72 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post from '../../../models/post'; +import Reaction from '../../../models/post-reaction'; + +/** + * Aggregate reactions of a post + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Lookup post + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); + + const reactions = await Reaction + .find({ + post_id: post._id, + $or: [ + { deleted_at: { $exists: false } }, + { deleted_at: { $gt: startTime } } + ] + }, { + sort: { + _id: -1 + }, + fields: { + _id: false, + post_id: false + } + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + let day = new Date(new Date().setDate(new Date().getDate() - i)); + day = new Date(day.setMilliseconds(999)); + day = new Date(day.setSeconds(59)); + day = new Date(day.setMinutes(59)); + day = new Date(day.setHours(23)); + // day = day.getTime(); + + const count = reactions.filter(r => + r.created_at < day && (r.deleted_at == null || r.deleted_at > day) + ).length; + + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: count + }); + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/posts/reply.ts b/src/server/api/endpoints/aggregation/posts/reply.ts new file mode 100644 index 0000000000..b114c34e1e --- /dev/null +++ b/src/server/api/endpoints/aggregation/posts/reply.ts @@ -0,0 +1,75 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post from '../../../models/post'; + +/** + * Aggregate reply of a post + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Lookup post + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + const datas = await Post + .aggregate([ + { $match: { reply: post._id } }, + { $project: { + created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + }}, + { $project: { + date: { + year: { $year: '$created_at' }, + month: { $month: '$created_at' }, + day: { $dayOfMonth: '$created_at' } + } + }}, + { $group: { + _id: '$date', + count: { $sum: 1 } + }} + ]); + + datas.forEach(data => { + data.date = data._id; + delete data._id; + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + const day = new Date(new Date().setDate(new Date().getDate() - i)); + + const data = datas.filter(d => + d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() + )[0]; + + if (data) { + graph.push(data); + } else { + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: 0 + }); + } + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/posts/repost.ts b/src/server/api/endpoints/aggregation/posts/repost.ts new file mode 100644 index 0000000000..217159caa7 --- /dev/null +++ b/src/server/api/endpoints/aggregation/posts/repost.ts @@ -0,0 +1,75 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post from '../../../models/post'; + +/** + * Aggregate repost of a post + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Lookup post + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + const datas = await Post + .aggregate([ + { $match: { repost_id: post._id } }, + { $project: { + created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + }}, + { $project: { + date: { + year: { $year: '$created_at' }, + month: { $month: '$created_at' }, + day: { $dayOfMonth: '$created_at' } + } + }}, + { $group: { + _id: '$date', + count: { $sum: 1 } + }} + ]); + + datas.forEach(data => { + data.date = data._id; + delete data._id; + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + const day = new Date(new Date().setDate(new Date().getDate() - i)); + + const data = datas.filter(d => + d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() + )[0]; + + if (data) { + graph.push(data); + } else { + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: 0 + }); + } + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts new file mode 100644 index 0000000000..e38ce92ff9 --- /dev/null +++ b/src/server/api/endpoints/aggregation/users.ts @@ -0,0 +1,61 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../models/user'; + +/** + * Aggregate users + * + * @param {any} params + * @return {Promise} + */ +module.exports = params => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 365, limitErr] = $(params.limit).optional.number().range(1, 365).$; + if (limitErr) return rej('invalid limit param'); + + const users = await User + .find({}, { + sort: { + _id: -1 + }, + fields: { + _id: false, + created_at: true, + deleted_at: true + } + }); + + const graph = []; + + for (let i = 0; i < limit; i++) { + let dayStart = new Date(new Date().setDate(new Date().getDate() - i)); + dayStart = new Date(dayStart.setMilliseconds(0)); + dayStart = new Date(dayStart.setSeconds(0)); + dayStart = new Date(dayStart.setMinutes(0)); + dayStart = new Date(dayStart.setHours(0)); + + let dayEnd = new Date(new Date().setDate(new Date().getDate() - i)); + dayEnd = new Date(dayEnd.setMilliseconds(999)); + dayEnd = new Date(dayEnd.setSeconds(59)); + dayEnd = new Date(dayEnd.setMinutes(59)); + dayEnd = new Date(dayEnd.setHours(23)); + // day = day.getTime(); + + const total = users.filter(u => + u.created_at < dayEnd && (u.deleted_at == null || u.deleted_at > dayEnd) + ).length; + + const created = users.filter(u => + u.created_at < dayEnd && u.created_at > dayStart + ).length; + + graph.push({ + total: total, + created: created + }); + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts new file mode 100644 index 0000000000..102a71d7cb --- /dev/null +++ b/src/server/api/endpoints/aggregation/users/activity.ts @@ -0,0 +1,116 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../models/user'; +import Post from '../../../models/post'; + +// TODO: likeやfollowも集計 + +/** + * Aggregate activity of a user + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 365, limitErr] = $(params.limit).optional.number().range(1, 365).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + const datas = await Post + .aggregate([ + { $match: { user_id: user._id } }, + { $project: { + repost_id: '$repost_id', + reply_id: '$reply_id', + created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + }}, + { $project: { + date: { + year: { $year: '$created_at' }, + month: { $month: '$created_at' }, + day: { $dayOfMonth: '$created_at' } + }, + type: { + $cond: { + if: { $ne: ['$repost_id', null] }, + then: 'repost', + else: { + $cond: { + if: { $ne: ['$reply_id', null] }, + then: 'reply', + else: 'post' + } + } + } + }} + }, + { $group: { _id: { + date: '$date', + type: '$type' + }, count: { $sum: 1 } } }, + { $group: { + _id: '$_id.date', + data: { $addToSet: { + type: '$_id.type', + count: '$count' + }} + } } + ]); + + datas.forEach(data => { + data.date = data._id; + delete data._id; + + data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count; + data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count; + data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count; + + delete data.data; + }); + + const graph = []; + + for (let i = 0; i < limit; i++) { + const day = new Date(new Date().setDate(new Date().getDate() - i)); + + const data = datas.filter(d => + d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() + )[0]; + + if (data) { + graph.push(data); + } else { + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + posts: 0, + reposts: 0, + replies: 0 + }); + } + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts new file mode 100644 index 0000000000..3022b2b002 --- /dev/null +++ b/src/server/api/endpoints/aggregation/users/followers.ts @@ -0,0 +1,74 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../models/user'; +import Following from '../../../models/following'; + +/** + * Aggregate followers of a user + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); + + const following = await Following + .find({ + followee_id: user._id, + $or: [ + { deleted_at: { $exists: false } }, + { deleted_at: { $gt: startTime } } + ] + }, { + _id: false, + follower_id: false, + followee_id: false + }, { + sort: { created_at: -1 } + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + let day = new Date(new Date().setDate(new Date().getDate() - i)); + day = new Date(day.setMilliseconds(999)); + day = new Date(day.setSeconds(59)); + day = new Date(day.setMinutes(59)); + day = new Date(day.setHours(23)); + // day = day.getTime(); + + const count = following.filter(f => + f.created_at < day && (f.deleted_at == null || f.deleted_at > day) + ).length; + + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: count + }); + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts new file mode 100644 index 0000000000..92da7e6921 --- /dev/null +++ b/src/server/api/endpoints/aggregation/users/following.ts @@ -0,0 +1,73 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../models/user'; +import Following from '../../../models/following'; + +/** + * Aggregate following of a user + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); + + const following = await Following + .find({ + follower_id: user._id, + $or: [ + { deleted_at: { $exists: false } }, + { deleted_at: { $gt: startTime } } + ] + }, { + _id: false, + follower_id: false, + followee_id: false + }, { + sort: { created_at: -1 } + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + let day = new Date(new Date().setDate(new Date().getDate() - i)); + day = new Date(day.setMilliseconds(999)); + day = new Date(day.setSeconds(59)); + day = new Date(day.setMinutes(59)); + day = new Date(day.setHours(23)); + + const count = following.filter(f => + f.created_at < day && (f.deleted_at == null || f.deleted_at > day) + ).length; + + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: count + }); + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts new file mode 100644 index 0000000000..c6a75eee39 --- /dev/null +++ b/src/server/api/endpoints/aggregation/users/post.ts @@ -0,0 +1,110 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../models/user'; +import Post from '../../../models/post'; + +/** + * Aggregate post of a user + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + const datas = await Post + .aggregate([ + { $match: { user_id: user._id } }, + { $project: { + repost_id: '$repost_id', + reply_id: '$reply_id', + created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + }}, + { $project: { + date: { + year: { $year: '$created_at' }, + month: { $month: '$created_at' }, + day: { $dayOfMonth: '$created_at' } + }, + type: { + $cond: { + if: { $ne: ['$repost_id', null] }, + then: 'repost', + else: { + $cond: { + if: { $ne: ['$reply_id', null] }, + then: 'reply', + else: 'post' + } + } + } + }} + }, + { $group: { _id: { + date: '$date', + type: '$type' + }, count: { $sum: 1 } } }, + { $group: { + _id: '$_id.date', + data: { $addToSet: { + type: '$_id.type', + count: '$count' + }} + } } + ]); + + datas.forEach(data => { + data.date = data._id; + delete data._id; + + data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count; + data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count; + data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count; + + delete data.data; + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + const day = new Date(new Date().setDate(new Date().getDate() - i)); + + const data = datas.filter(d => + d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() + )[0]; + + if (data) { + graph.push(data); + } else { + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + posts: 0, + reposts: 0, + replies: 0 + }); + } + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts new file mode 100644 index 0000000000..0a082ed1b7 --- /dev/null +++ b/src/server/api/endpoints/aggregation/users/reaction.ts @@ -0,0 +1,80 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../models/user'; +import Reaction from '../../../models/post-reaction'; + +/** + * Aggregate reaction of a user + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + const datas = await Reaction + .aggregate([ + { $match: { user_id: user._id } }, + { $project: { + created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + }}, + { $project: { + date: { + year: { $year: '$created_at' }, + month: { $month: '$created_at' }, + day: { $dayOfMonth: '$created_at' } + } + }}, + { $group: { + _id: '$date', + count: { $sum: 1 } + }} + ]); + + datas.forEach(data => { + data.date = data._id; + delete data._id; + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + const day = new Date(new Date().setDate(new Date().getDate() - i)); + + const data = datas.filter(d => + d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() + )[0]; + + if (data) { + graph.push(data); + } else { + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: 0 + }); + } + } + + res(graph); +}); diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts new file mode 100644 index 0000000000..0f688792a7 --- /dev/null +++ b/src/server/api/endpoints/app/create.ts @@ -0,0 +1,108 @@ +/** + * Module dependencies + */ +import rndstr from 'rndstr'; +import $ from 'cafy'; +import App, { isValidNameId, pack } from '../../models/app'; + +/** + * @swagger + * /app/create: + * post: + * summary: Create an application + * parameters: + * - $ref: "#/parameters/AccessToken" + * - + * name: name_id + * description: Application unique name + * in: formData + * required: true + * type: string + * - + * name: name + * description: Application name + * in: formData + * required: true + * type: string + * - + * name: description + * description: Application description + * in: formData + * required: true + * type: string + * - + * name: permission + * description: Permissions that application has + * in: formData + * required: true + * type: array + * items: + * type: string + * collectionFormat: csv + * - + * name: callback_url + * description: URL called back after authentication + * in: formData + * required: false + * type: string + * + * responses: + * 200: + * description: Created application's information + * schema: + * $ref: "#/definitions/Application" + * + * default: + * description: Failed + * schema: + * $ref: "#/definitions/Error" + */ + +/** + * Create an app + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'name_id' parameter + const [nameId, nameIdErr] = $(params.name_id).string().pipe(isValidNameId).$; + if (nameIdErr) return rej('invalid name_id param'); + + // Get 'name' parameter + const [name, nameErr] = $(params.name).string().$; + if (nameErr) return rej('invalid name param'); + + // Get 'description' parameter + const [description, descriptionErr] = $(params.description).string().$; + if (descriptionErr) return rej('invalid description param'); + + // Get 'permission' parameter + const [permission, permissionErr] = $(params.permission).array('string').unique().$; + if (permissionErr) return rej('invalid permission param'); + + // Get 'callback_url' parameter + // TODO: Check it is valid url + const [callbackUrl = null, callbackUrlErr] = $(params.callback_url).optional.nullable.string().$; + if (callbackUrlErr) return rej('invalid callback_url param'); + + // Generate secret + const secret = rndstr('a-zA-Z0-9', 32); + + // Create account + const app = await App.insert({ + created_at: new Date(), + user_id: user._id, + name: name, + name_id: nameId, + name_id_lower: nameId.toLowerCase(), + description: description, + permission: permission, + callback_url: callbackUrl, + secret: secret + }); + + // Response + res(await pack(app)); +}); diff --git a/src/server/api/endpoints/app/name_id/available.ts b/src/server/api/endpoints/app/name_id/available.ts new file mode 100644 index 0000000000..3d2c710322 --- /dev/null +++ b/src/server/api/endpoints/app/name_id/available.ts @@ -0,0 +1,60 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import App from '../../../models/app'; +import { isValidNameId } from '../../../models/app'; + +/** + * @swagger + * /app/name_id/available: + * post: + * summary: Check available name_id on creation an application + * parameters: + * - + * name: name_id + * description: Application unique name + * in: formData + * required: true + * type: string + * + * responses: + * 200: + * description: Success + * schema: + * type: object + * properties: + * available: + * description: Whether name_id is available + * type: boolean + * + * default: + * description: Failed + * schema: + * $ref: "#/definitions/Error" + */ + +/** + * Check available name_id of app + * + * @param {any} params + * @return {Promise} + */ +module.exports = async (params) => new Promise(async (res, rej) => { + // Get 'name_id' parameter + const [nameId, nameIdErr] = $(params.name_id).string().pipe(isValidNameId).$; + if (nameIdErr) return rej('invalid name_id param'); + + // Get exist + const exist = await App + .count({ + name_id_lower: nameId.toLowerCase() + }, { + limit: 1 + }); + + // Reply + res({ + available: exist === 0 + }); +}); diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts new file mode 100644 index 0000000000..8bc3dda42c --- /dev/null +++ b/src/server/api/endpoints/app/show.ts @@ -0,0 +1,72 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import App, { pack } from '../../models/app'; + +/** + * @swagger + * /app/show: + * post: + * summary: Show an application's information + * description: Require app_id or name_id + * parameters: + * - + * name: app_id + * description: Application ID + * in: formData + * type: string + * - + * name: name_id + * description: Application unique name + * in: formData + * type: string + * + * responses: + * 200: + * description: Success + * schema: + * $ref: "#/definitions/Application" + * + * default: + * description: Failed + * schema: + * $ref: "#/definitions/Error" + */ + +/** + * Show an app + * + * @param {any} params + * @param {any} user + * @param {any} _ + * @param {any} isSecure + * @return {Promise} + */ +module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => { + // Get 'app_id' parameter + const [appId, appIdErr] = $(params.app_id).optional.id().$; + if (appIdErr) return rej('invalid app_id param'); + + // Get 'name_id' parameter + const [nameId, nameIdErr] = $(params.name_id).optional.string().$; + if (nameIdErr) return rej('invalid name_id param'); + + if (appId === undefined && nameId === undefined) { + return rej('app_id or name_id is required'); + } + + // Lookup app + const app = appId !== undefined + ? await App.findOne({ _id: appId }) + : await App.findOne({ name_id_lower: nameId.toLowerCase() }); + + if (app === null) { + return rej('app not found'); + } + + // Send response + res(await pack(app, user, { + includeSecret: isSecure && app.user_id.equals(user._id) + })); +}); diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts new file mode 100644 index 0000000000..4ee20a6d25 --- /dev/null +++ b/src/server/api/endpoints/auth/accept.ts @@ -0,0 +1,93 @@ +/** + * Module dependencies + */ +import rndstr from 'rndstr'; +const crypto = require('crypto'); +import $ from 'cafy'; +import App from '../../models/app'; +import AuthSess from '../../models/auth-session'; +import AccessToken from '../../models/access-token'; + +/** + * @swagger + * /auth/accept: + * post: + * summary: Accept a session + * parameters: + * - $ref: "#/parameters/NativeToken" + * - + * name: token + * description: Session Token + * in: formData + * required: true + * type: string + * responses: + * 204: + * description: OK + * + * default: + * description: Failed + * schema: + * $ref: "#/definitions/Error" + */ + +/** + * Accept + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'token' parameter + const [token, tokenErr] = $(params.token).string().$; + if (tokenErr) return rej('invalid token param'); + + // Fetch token + const session = await AuthSess + .findOne({ token: token }); + + if (session === null) { + return rej('session not found'); + } + + // Generate access token + const accessToken = rndstr('a-zA-Z0-9', 32); + + // Fetch exist access token + const exist = await AccessToken.findOne({ + app_id: session.app_id, + user_id: user._id, + }); + + if (exist === null) { + // Lookup app + const app = await App.findOne({ + _id: session.app_id + }); + + // Generate Hash + const sha256 = crypto.createHash('sha256'); + sha256.update(accessToken + app.secret); + const hash = sha256.digest('hex'); + + // Insert access token doc + await AccessToken.insert({ + created_at: new Date(), + app_id: session.app_id, + user_id: user._id, + token: accessToken, + hash: hash + }); + } + + // Update session + await AuthSess.update(session._id, { + $set: { + user_id: user._id + } + }); + + // Response + res(); +}); diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts new file mode 100644 index 0000000000..dc6a045b6e --- /dev/null +++ b/src/server/api/endpoints/auth/session/generate.ts @@ -0,0 +1,76 @@ +/** + * Module dependencies + */ +import * as uuid from 'uuid'; +import $ from 'cafy'; +import App from '../../../models/app'; +import AuthSess from '../../../models/auth-session'; +import config from '../../../../../conf'; + +/** + * @swagger + * /auth/session/generate: + * post: + * summary: Generate a session + * parameters: + * - + * name: app_secret + * description: App Secret + * in: formData + * required: true + * type: string + * + * responses: + * 200: + * description: OK + * schema: + * type: object + * properties: + * token: + * type: string + * description: Session Token + * url: + * type: string + * description: Authentication form's URL + * default: + * description: Failed + * schema: + * $ref: "#/definitions/Error" + */ + +/** + * Generate a session + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'app_secret' parameter + const [appSecret, appSecretErr] = $(params.app_secret).string().$; + if (appSecretErr) return rej('invalid app_secret param'); + + // Lookup app + const app = await App.findOne({ + secret: appSecret + }); + + if (app == null) { + return rej('app not found'); + } + + // Generate token + const token = uuid.v4(); + + // Create session token document + const doc = await AuthSess.insert({ + created_at: new Date(), + app_id: app._id, + token: token + }); + + // Response + res({ + token: doc.token, + url: `${config.auth_url}/${doc.token}` + }); +}); diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts new file mode 100644 index 0000000000..73ac3185f6 --- /dev/null +++ b/src/server/api/endpoints/auth/session/show.ts @@ -0,0 +1,70 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import AuthSess, { pack } from '../../../models/auth-session'; + +/** + * @swagger + * /auth/session/show: + * post: + * summary: Show a session information + * parameters: + * - + * name: token + * description: Session Token + * in: formData + * required: true + * type: string + * + * responses: + * 200: + * description: OK + * schema: + * type: object + * properties: + * created_at: + * type: string + * format: date-time + * description: Date and time of the session creation + * app_id: + * type: string + * description: Application ID + * token: + * type: string + * description: Session Token + * user_id: + * type: string + * description: ID of user who create the session + * app: + * $ref: "#/definitions/Application" + * default: + * description: Failed + * schema: + * $ref: "#/definitions/Error" + */ + +/** + * Show a session + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'token' parameter + const [token, tokenErr] = $(params.token).string().$; + if (tokenErr) return rej('invalid token param'); + + // Lookup session + const session = await AuthSess.findOne({ + token: token + }); + + if (session == null) { + return rej('session not found'); + } + + // Response + res(await pack(session, user)); +}); diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts new file mode 100644 index 0000000000..fc989bf8c2 --- /dev/null +++ b/src/server/api/endpoints/auth/session/userkey.ts @@ -0,0 +1,109 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import App from '../../../models/app'; +import AuthSess from '../../../models/auth-session'; +import AccessToken from '../../../models/access-token'; +import { pack } from '../../../models/user'; + +/** + * @swagger + * /auth/session/userkey: + * post: + * summary: Get an access token(userkey) + * parameters: + * - + * name: app_secret + * description: App Secret + * in: formData + * required: true + * type: string + * - + * name: token + * description: Session Token + * in: formData + * required: true + * type: string + * + * responses: + * 200: + * description: OK + * schema: + * type: object + * properties: + * userkey: + * type: string + * description: Access Token + * user: + * $ref: "#/definitions/User" + * default: + * description: Failed + * schema: + * $ref: "#/definitions/Error" + */ + +/** + * Generate a session + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'app_secret' parameter + const [appSecret, appSecretErr] = $(params.app_secret).string().$; + if (appSecretErr) return rej('invalid app_secret param'); + + // Lookup app + const app = await App.findOne({ + secret: appSecret + }); + + if (app == null) { + return rej('app not found'); + } + + // Get 'token' parameter + const [token, tokenErr] = $(params.token).string().$; + if (tokenErr) return rej('invalid token param'); + + // Fetch token + const session = await AuthSess + .findOne({ + token: token, + app_id: app._id + }); + + if (session === null) { + return rej('session not found'); + } + + if (session.user_id == null) { + return rej('this session is not allowed yet'); + } + + // Lookup access token + const accessToken = await AccessToken.findOne({ + app_id: app._id, + user_id: session.user_id + }); + + // Delete session + + /* https://github.com/Automattic/monk/issues/178 + AuthSess.deleteOne({ + _id: session._id + }); + */ + AuthSess.remove({ + _id: session._id + }); + + // Response + res({ + access_token: accessToken.token, + user: await pack(session.user_id, null, { + detail: true + }) + }); +}); diff --git a/src/server/api/endpoints/channels.ts b/src/server/api/endpoints/channels.ts new file mode 100644 index 0000000000..b9a7d1b788 --- /dev/null +++ b/src/server/api/endpoints/channels.ts @@ -0,0 +1,58 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel, { pack } from '../models/channel'; + +/** + * Get all channels + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + // Construct query + const sort = { + _id: -1 + }; + const query = {} as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const channels = await Channel + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(channels.map(async channel => + await pack(channel, me)))); +}); diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts new file mode 100644 index 0000000000..695b4515b3 --- /dev/null +++ b/src/server/api/endpoints/channels/create.ts @@ -0,0 +1,39 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../../models/channel'; +import Watching from '../../models/channel-watching'; +import { pack } from '../../models/channel'; + +/** + * Create a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'title' parameter + const [title, titleErr] = $(params.title).string().range(1, 100).$; + if (titleErr) return rej('invalid title param'); + + // Create a channel + const channel = await Channel.insert({ + created_at: new Date(), + user_id: user._id, + title: title, + index: 0, + watching_count: 1 + }); + + // Response + res(await pack(channel)); + + // Create Watching + await Watching.insert({ + created_at: new Date(), + user_id: user._id, + channel_id: channel._id + }); +}); diff --git a/src/server/api/endpoints/channels/posts.ts b/src/server/api/endpoints/channels/posts.ts new file mode 100644 index 0000000000..d722589c20 --- /dev/null +++ b/src/server/api/endpoints/channels/posts.ts @@ -0,0 +1,78 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import { default as Channel, IChannel } from '../../models/channel'; +import Post, { pack } from '../../models/post'; + +/** + * Show a posts of a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 1000, limitErr] = $(params.limit).optional.number().range(1, 1000).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + // Get 'channel_id' parameter + const [channelId, channelIdErr] = $(params.channel_id).id().$; + if (channelIdErr) return rej('invalid channel_id param'); + + // Fetch channel + const channel: IChannel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + //#region Construct query + const sort = { + _id: -1 + }; + + const query = { + channel_id: channel._id + } as any; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + //#endregion Construct query + + // Issue query + const posts = await Post + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(posts.map(async (post) => + await pack(post, user) + ))); +}); diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts new file mode 100644 index 0000000000..332da64675 --- /dev/null +++ b/src/server/api/endpoints/channels/show.ts @@ -0,0 +1,30 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel, { IChannel, pack } from '../../models/channel'; + +/** + * Show a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'channel_id' parameter + const [channelId, channelIdErr] = $(params.channel_id).id().$; + if (channelIdErr) return rej('invalid channel_id param'); + + // Fetch channel + const channel: IChannel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + // Serialize + res(await pack(channel, user)); +}); diff --git a/src/server/api/endpoints/channels/unwatch.ts b/src/server/api/endpoints/channels/unwatch.ts new file mode 100644 index 0000000000..19d3be118a --- /dev/null +++ b/src/server/api/endpoints/channels/unwatch.ts @@ -0,0 +1,60 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../../models/channel'; +import Watching from '../../models/channel-watching'; + +/** + * Unwatch a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'channel_id' parameter + const [channelId, channelIdErr] = $(params.channel_id).id().$; + if (channelIdErr) return rej('invalid channel_id param'); + + //#region Fetch channel + const channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + //#endregion + + //#region Check whether not watching + const exist = await Watching.findOne({ + user_id: user._id, + channel_id: channel._id, + deleted_at: { $exists: false } + }); + + if (exist === null) { + return rej('already not watching'); + } + //#endregion + + // Delete watching + await Watching.update({ + _id: exist._id + }, { + $set: { + deleted_at: new Date() + } + }); + + // Send response + res(); + + // Decrement watching count + Channel.update(channel._id, { + $inc: { + watching_count: -1 + } + }); +}); diff --git a/src/server/api/endpoints/channels/watch.ts b/src/server/api/endpoints/channels/watch.ts new file mode 100644 index 0000000000..030e0dd411 --- /dev/null +++ b/src/server/api/endpoints/channels/watch.ts @@ -0,0 +1,58 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../../models/channel'; +import Watching from '../../models/channel-watching'; + +/** + * Watch a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'channel_id' parameter + const [channelId, channelIdErr] = $(params.channel_id).id().$; + if (channelIdErr) return rej('invalid channel_id param'); + + //#region Fetch channel + const channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + //#endregion + + //#region Check whether already watching + const exist = await Watching.findOne({ + user_id: user._id, + channel_id: channel._id, + deleted_at: { $exists: false } + }); + + if (exist !== null) { + return rej('already watching'); + } + //#endregion + + // Create Watching + await Watching.insert({ + created_at: new Date(), + user_id: user._id, + channel_id: channel._id + }); + + // Send response + res(); + + // Increment watching count + Channel.update(channel._id, { + $inc: { + watching_count: 1 + } + }); +}); diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts new file mode 100644 index 0000000000..d92473633a --- /dev/null +++ b/src/server/api/endpoints/drive.ts @@ -0,0 +1,37 @@ +/** + * Module dependencies + */ +import DriveFile from '../models/drive-file'; + +/** + * Get drive information + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Calculate drive usage + const usage = ((await DriveFile + .aggregate([ + { $match: { 'metadata.user_id': user._id } }, + { + $project: { + length: true + } + }, + { + $group: { + _id: null, + usage: { $sum: '$length' } + } + } + ]))[0] || { + usage: 0 + }).usage; + + res({ + capacity: user.drive_capacity, + usage: usage + }); +}); diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts new file mode 100644 index 0000000000..89915331ea --- /dev/null +++ b/src/server/api/endpoints/drive/files.ts @@ -0,0 +1,73 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFile, { pack } from '../../models/drive-file'; + +/** + * Get drive files + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise} + */ +module.exports = async (params, user, app) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) throw 'invalid limit param'; + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) throw 'invalid since_id param'; + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) throw 'invalid until_id param'; + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + throw 'cannot set since_id and until_id'; + } + + // Get 'folder_id' parameter + const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; + if (folderIdErr) throw 'invalid folder_id param'; + + // Get 'type' parameter + const [type, typeErr] = $(params.type).optional.string().match(/^[a-zA-Z\/\-\*]+$/).$; + if (typeErr) throw 'invalid type param'; + + // Construct query + const sort = { + _id: -1 + }; + const query = { + 'metadata.user_id': user._id, + 'metadata.folder_id': folderId + } as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + if (type) { + query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); + } + + // Issue query + const files = await DriveFile + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + const _files = await Promise.all(files.map(file => pack(file))); + return _files; +}; diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts new file mode 100644 index 0000000000..db801b61fe --- /dev/null +++ b/src/server/api/endpoints/drive/files/create.ts @@ -0,0 +1,51 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import { validateFileName, pack } from '../../../models/drive-file'; +import create from '../../../common/drive/add-file'; + +/** + * Create a file + * + * @param {any} file + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = async (file, params, user): Promise => { + if (file == null) { + throw 'file is required'; + } + + // Get 'name' parameter + let name = file.originalname; + if (name !== undefined && name !== null) { + name = name.trim(); + if (name.length === 0) { + name = null; + } else if (name === 'blob') { + name = null; + } else if (!validateFileName(name)) { + throw 'invalid name'; + } + } else { + name = null; + } + + // Get 'folder_id' parameter + const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; + if (folderIdErr) throw 'invalid folder_id param'; + + try { + // Create file + const driveFile = await create(user, file.path, name, null, folderId); + + // Serialize + return pack(driveFile); + } catch (e) { + console.error(e); + + throw e; + } +}; diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts new file mode 100644 index 0000000000..e026afe936 --- /dev/null +++ b/src/server/api/endpoints/drive/files/find.ts @@ -0,0 +1,34 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFile, { pack } from '../../../models/drive-file'; + +/** + * Find a file(s) + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'name' parameter + const [name, nameErr] = $(params.name).string().$; + if (nameErr) return rej('invalid name param'); + + // Get 'folder_id' parameter + const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; + if (folderIdErr) return rej('invalid folder_id param'); + + // Issue query + const files = await DriveFile + .find({ + filename: name, + 'metadata.user_id': user._id, + 'metadata.folder_id': folderId + }); + + // Serialize + res(await Promise.all(files.map(async file => + await pack(file)))); +}); diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts new file mode 100644 index 0000000000..21664f7ba4 --- /dev/null +++ b/src/server/api/endpoints/drive/files/show.ts @@ -0,0 +1,36 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFile, { pack } from '../../../models/drive-file'; + +/** + * Show a file + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = async (params, user) => { + // Get 'file_id' parameter + const [fileId, fileIdErr] = $(params.file_id).id().$; + if (fileIdErr) throw 'invalid file_id param'; + + // Fetch file + const file = await DriveFile + .findOne({ + _id: fileId, + 'metadata.user_id': user._id + }); + + if (file === null) { + throw 'file-not-found'; + } + + // Serialize + const _file = await pack(file, { + detail: true + }); + + return _file; +}; diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts new file mode 100644 index 0000000000..83da462113 --- /dev/null +++ b/src/server/api/endpoints/drive/files/update.ts @@ -0,0 +1,75 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFolder from '../../../models/drive-folder'; +import DriveFile, { validateFileName, pack } from '../../../models/drive-file'; +import { publishDriveStream } from '../../../event'; + +/** + * Update a file + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'file_id' parameter + const [fileId, fileIdErr] = $(params.file_id).id().$; + if (fileIdErr) return rej('invalid file_id param'); + + // Fetch file + const file = await DriveFile + .findOne({ + _id: fileId, + 'metadata.user_id': user._id + }); + + if (file === null) { + return rej('file-not-found'); + } + + // Get 'name' parameter + const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$; + if (nameErr) return rej('invalid name param'); + if (name) file.filename = name; + + // Get 'folder_id' parameter + const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$; + if (folderIdErr) return rej('invalid folder_id param'); + + if (folderId !== undefined) { + if (folderId === null) { + file.metadata.folder_id = null; + } else { + // Fetch folder + const folder = await DriveFolder + .findOne({ + _id: folderId, + user_id: user._id + }); + + if (folder === null) { + return rej('folder-not-found'); + } + + file.metadata.folder_id = folder._id; + } + } + + await DriveFile.update(file._id, { + $set: { + filename: file.filename, + 'metadata.folder_id': file.metadata.folder_id + } + }); + + // Serialize + const fileObj = await pack(file); + + // Response + res(fileObj); + + // Publish file_updated event + publishDriveStream(user._id, 'file_updated', fileObj); +}); diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts new file mode 100644 index 0000000000..346633c616 --- /dev/null +++ b/src/server/api/endpoints/drive/files/upload_from_url.ts @@ -0,0 +1,26 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import { pack } from '../../../models/drive-file'; +import uploadFromUrl from '../../../common/drive/upload_from_url'; + +/** + * Create a file from a URL + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = async (params, user): Promise => { + // Get 'url' parameter + // TODO: Validate this url + const [url, urlErr] = $(params.url).string().$; + if (urlErr) throw 'invalid url param'; + + // Get 'folder_id' parameter + const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; + if (folderIdErr) throw 'invalid folder_id param'; + + return pack(await uploadFromUrl(url, user, folderId)); +}; diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts new file mode 100644 index 0000000000..428bde3507 --- /dev/null +++ b/src/server/api/endpoints/drive/folders.ts @@ -0,0 +1,66 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFolder, { pack } from '../../models/drive-folder'; + +/** + * Get drive folders + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise} + */ +module.exports = (params, user, app) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + // Get 'folder_id' parameter + const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; + if (folderIdErr) return rej('invalid folder_id param'); + + // Construct query + const sort = { + _id: -1 + }; + const query = { + user_id: user._id, + parent_id: folderId + } as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const folders = await DriveFolder + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(folders.map(async folder => + await pack(folder)))); +}); diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts new file mode 100644 index 0000000000..03f396ddc9 --- /dev/null +++ b/src/server/api/endpoints/drive/folders/create.ts @@ -0,0 +1,55 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder'; +import { publishDriveStream } from '../../../event'; + +/** + * Create drive folder + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'name' parameter + const [name = '無題のフォルダー', nameErr] = $(params.name).optional.string().pipe(isValidFolderName).$; + if (nameErr) return rej('invalid name param'); + + // Get 'parent_id' parameter + const [parentId = null, parentIdErr] = $(params.parent_id).optional.nullable.id().$; + if (parentIdErr) return rej('invalid parent_id param'); + + // If the parent folder is specified + let parent = null; + if (parentId) { + // Fetch parent folder + parent = await DriveFolder + .findOne({ + _id: parentId, + user_id: user._id + }); + + if (parent === null) { + return rej('parent-not-found'); + } + } + + // Create folder + const folder = await DriveFolder.insert({ + created_at: new Date(), + name: name, + parent_id: parent !== null ? parent._id : null, + user_id: user._id + }); + + // Serialize + const folderObj = await pack(folder); + + // Response + res(folderObj); + + // Publish folder_created event + publishDriveStream(user._id, 'folder_created', folderObj); +}); diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts new file mode 100644 index 0000000000..fc84766bc8 --- /dev/null +++ b/src/server/api/endpoints/drive/folders/find.ts @@ -0,0 +1,33 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFolder, { pack } from '../../../models/drive-folder'; + +/** + * Find a folder(s) + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'name' parameter + const [name, nameErr] = $(params.name).string().$; + if (nameErr) return rej('invalid name param'); + + // Get 'parent_id' parameter + const [parentId = null, parentIdErr] = $(params.parent_id).optional.nullable.id().$; + if (parentIdErr) return rej('invalid parent_id param'); + + // Issue query + const folders = await DriveFolder + .find({ + name: name, + user_id: user._id, + parent_id: parentId + }); + + // Serialize + res(await Promise.all(folders.map(folder => pack(folder)))); +}); diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts new file mode 100644 index 0000000000..e07d14d20d --- /dev/null +++ b/src/server/api/endpoints/drive/folders/show.ts @@ -0,0 +1,34 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFolder, { pack } from '../../../models/drive-folder'; + +/** + * Show a folder + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'folder_id' parameter + const [folderId, folderIdErr] = $(params.folder_id).id().$; + if (folderIdErr) return rej('invalid folder_id param'); + + // Get folder + const folder = await DriveFolder + .findOne({ + _id: folderId, + user_id: user._id + }); + + if (folder === null) { + return rej('folder-not-found'); + } + + // Serialize + res(await pack(folder, { + detail: true + })); +}); diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts new file mode 100644 index 0000000000..d3df8bdae5 --- /dev/null +++ b/src/server/api/endpoints/drive/folders/update.ts @@ -0,0 +1,99 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder'; +import { publishDriveStream } from '../../../event'; + +/** + * Update a folder + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'folder_id' parameter + const [folderId, folderIdErr] = $(params.folder_id).id().$; + if (folderIdErr) return rej('invalid folder_id param'); + + // Fetch folder + const folder = await DriveFolder + .findOne({ + _id: folderId, + user_id: user._id + }); + + if (folder === null) { + return rej('folder-not-found'); + } + + // Get 'name' parameter + const [name, nameErr] = $(params.name).optional.string().pipe(isValidFolderName).$; + if (nameErr) return rej('invalid name param'); + if (name) folder.name = name; + + // Get 'parent_id' parameter + const [parentId, parentIdErr] = $(params.parent_id).optional.nullable.id().$; + if (parentIdErr) return rej('invalid parent_id param'); + if (parentId !== undefined) { + if (parentId === null) { + folder.parent_id = null; + } else { + // Get parent folder + const parent = await DriveFolder + .findOne({ + _id: parentId, + user_id: user._id + }); + + if (parent === null) { + return rej('parent-folder-not-found'); + } + + // Check if the circular reference will occur + async function checkCircle(folderId) { + // Fetch folder + const folder2 = await DriveFolder.findOne({ + _id: folderId + }, { + _id: true, + parent_id: true + }); + + if (folder2._id.equals(folder._id)) { + return true; + } else if (folder2.parent_id) { + return await checkCircle(folder2.parent_id); + } else { + return false; + } + } + + if (parent.parent_id !== null) { + if (await checkCircle(parent.parent_id)) { + return rej('detected-circular-definition'); + } + } + + folder.parent_id = parent._id; + } + } + + // Update + DriveFolder.update(folder._id, { + $set: { + name: folder.name, + parent_id: folder.parent_id + } + }); + + // Serialize + const folderObj = await pack(folder); + + // Response + res(folderObj); + + // Publish folder_updated event + publishDriveStream(user._id, 'folder_updated', folderObj); +}); diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts new file mode 100644 index 0000000000..8352c7dd4c --- /dev/null +++ b/src/server/api/endpoints/drive/stream.ts @@ -0,0 +1,67 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFile, { pack } from '../../models/drive-file'; + +/** + * Get drive stream + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + // Get 'type' parameter + const [type, typeErr] = $(params.type).optional.string().match(/^[a-zA-Z\/\-\*]+$/).$; + if (typeErr) return rej('invalid type param'); + + // Construct query + const sort = { + _id: -1 + }; + const query = { + 'metadata.user_id': user._id + } as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + if (type) { + query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); + } + + // Issue query + const files = await DriveFile + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(files.map(async file => + await pack(file)))); +}); diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts new file mode 100644 index 0000000000..767b837b35 --- /dev/null +++ b/src/server/api/endpoints/following/create.ts @@ -0,0 +1,84 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { pack as packUser } from '../../models/user'; +import Following from '../../models/following'; +import notify from '../../common/notify'; +import event from '../../event'; + +/** + * Follow a user + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const follower = user; + + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // 自分自身 + if (user._id.equals(userId)) { + return rej('followee is yourself'); + } + + // Get followee + const followee = await User.findOne({ + _id: userId + }, { + fields: { + data: false, + 'account.profile': false + } + }); + + if (followee === null) { + return rej('user not found'); + } + + // Check if already following + const exist = await Following.findOne({ + follower_id: follower._id, + followee_id: followee._id, + deleted_at: { $exists: false } + }); + + if (exist !== null) { + return rej('already following'); + } + + // Create following + await Following.insert({ + created_at: new Date(), + follower_id: follower._id, + followee_id: followee._id + }); + + // Send response + res(); + + // Increment following count + User.update(follower._id, { + $inc: { + following_count: 1 + } + }); + + // Increment followers count + User.update({ _id: followee._id }, { + $inc: { + followers_count: 1 + } + }); + + // Publish follow event + event(follower._id, 'follow', await packUser(followee, follower)); + event(followee._id, 'followed', await packUser(follower, followee)); + + // Notify + notify(followee._id, follower._id, 'follow'); +}); diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts new file mode 100644 index 0000000000..64b9a8cecb --- /dev/null +++ b/src/server/api/endpoints/following/delete.ts @@ -0,0 +1,81 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { pack as packUser } from '../../models/user'; +import Following from '../../models/following'; +import event from '../../event'; + +/** + * Unfollow a user + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const follower = user; + + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Check if the followee is yourself + if (user._id.equals(userId)) { + return rej('followee is yourself'); + } + + // Get followee + const followee = await User.findOne({ + _id: userId + }, { + fields: { + data: false, + 'account.profile': false + } + }); + + if (followee === null) { + return rej('user not found'); + } + + // Check not following + const exist = await Following.findOne({ + follower_id: follower._id, + followee_id: followee._id, + deleted_at: { $exists: false } + }); + + if (exist === null) { + return rej('already not following'); + } + + // Delete following + await Following.update({ + _id: exist._id + }, { + $set: { + deleted_at: new Date() + } + }); + + // Send response + res(); + + // Decrement following count + User.update({ _id: follower._id }, { + $inc: { + following_count: -1 + } + }); + + // Decrement followers count + User.update({ _id: followee._id }, { + $inc: { + followers_count: -1 + } + }); + + // Publish follow event + event(follower._id, 'unfollow', await packUser(followee, follower)); +}); diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts new file mode 100644 index 0000000000..32b0382faf --- /dev/null +++ b/src/server/api/endpoints/i.ts @@ -0,0 +1,28 @@ +/** + * Module dependencies + */ +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 + res(await pack(user, user, { + detail: true, + includeSecrets: isSecure + })); + + // Update lastUsedAt + User.update({ _id: user._id }, { + $set: { + 'account.last_used_at': new Date() + } + }); +}); diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts new file mode 100644 index 0000000000..0f1db73829 --- /dev/null +++ b/src/server/api/endpoints/i/2fa/done.ts @@ -0,0 +1,37 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import * as speakeasy from 'speakeasy'; +import User from '../../../models/user'; + +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'token' parameter + const [token, tokenErr] = $(params.token).string().$; + if (tokenErr) return rej('invalid token param'); + + const _token = token.replace(/\s/g, ''); + + if (user.two_factor_temp_secret == null) { + return rej('二段階認証の設定が開始されていません'); + } + + const verified = (speakeasy as any).totp.verify({ + secret: user.two_factor_temp_secret, + encoding: 'base32', + token: _token + }); + + if (!verified) { + return rej('not verified'); + } + + await User.update(user._id, { + $set: { + 'account.two_factor_secret': user.two_factor_temp_secret, + 'account.two_factor_enabled': true + } + }); + + res(); +}); diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts new file mode 100644 index 0000000000..e2cc1487b8 --- /dev/null +++ b/src/server/api/endpoints/i/2fa/register.ts @@ -0,0 +1,48 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import * as bcrypt from 'bcryptjs'; +import * as speakeasy from 'speakeasy'; +import * as QRCode from 'qrcode'; +import User from '../../../models/user'; +import config from '../../../../../conf'; + +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'password' parameter + const [password, passwordErr] = $(params.password).string().$; + if (passwordErr) return rej('invalid password param'); + + // Compare password + const same = await bcrypt.compare(password, user.account.password); + + if (!same) { + return rej('incorrect password'); + } + + // Generate user's secret key + const secret = speakeasy.generateSecret({ + length: 32 + }); + + await User.update(user._id, { + $set: { + two_factor_temp_secret: secret.base32 + } + }); + + // Get the data URL of the authenticator URL + QRCode.toDataURL(speakeasy.otpauthURL({ + secret: secret.base32, + encoding: 'base32', + label: user.username, + issuer: config.host + }), (err, data_url) => { + res({ + qr: data_url, + secret: secret.base32, + label: user.username, + issuer: config.host + }); + }); +}); diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts new file mode 100644 index 0000000000..c43f9ccc44 --- /dev/null +++ b/src/server/api/endpoints/i/2fa/unregister.ts @@ -0,0 +1,28 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import * as bcrypt from 'bcryptjs'; +import User from '../../../models/user'; + +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'password' parameter + const [password, passwordErr] = $(params.password).string().$; + if (passwordErr) return rej('invalid password param'); + + // Compare password + const same = await bcrypt.compare(password, user.account.password); + + if (!same) { + return rej('incorrect password'); + } + + await User.update(user._id, { + $set: { + 'account.two_factor_secret': null, + 'account.two_factor_enabled': false + } + }); + + res(); +}); diff --git a/src/server/api/endpoints/i/appdata/get.ts b/src/server/api/endpoints/i/appdata/get.ts new file mode 100644 index 0000000000..571208d46c --- /dev/null +++ b/src/server/api/endpoints/i/appdata/get.ts @@ -0,0 +1,39 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Appdata from '../../../models/appdata'; + +/** + * Get app data + * + * @param {any} params + * @param {any} user + * @param {any} app + * @param {Boolean} isSecure + * @return {Promise} + */ +module.exports = (params, user, app) => new Promise(async (res, rej) => { + if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます'); + + // Get 'key' parameter + const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$; + if (keyError) return rej('invalid key param'); + + const select = {}; + if (key !== null) { + select[`data.${key}`] = true; + } + const appdata = await Appdata.findOne({ + app_id: app._id, + user_id: user._id + }, { + fields: select + }); + + if (appdata) { + res(appdata.data); + } else { + res(); + } +}); diff --git a/src/server/api/endpoints/i/appdata/set.ts b/src/server/api/endpoints/i/appdata/set.ts new file mode 100644 index 0000000000..2804a14cb3 --- /dev/null +++ b/src/server/api/endpoints/i/appdata/set.ts @@ -0,0 +1,58 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Appdata from '../../../models/appdata'; + +/** + * Set app data + * + * @param {any} params + * @param {any} user + * @param {any} app + * @param {Boolean} isSecure + * @return {Promise} + */ +module.exports = (params, user, app) => new Promise(async (res, rej) => { + if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます'); + + // Get 'data' parameter + const [data, dataError] = $(params.data).optional.object() + .pipe(obj => { + const hasInvalidData = Object.entries(obj).some(([k, v]) => + $(k).string().match(/^[a-z_]+$/).nok() && $(v).string().nok()); + return !hasInvalidData; + }).$; + if (dataError) return rej('invalid data param'); + + // Get 'key' parameter + const [key, keyError] = $(params.key).optional.string().match(/[a-z_]+/).$; + if (keyError) return rej('invalid key param'); + + // Get 'value' parameter + const [value, valueError] = $(params.value).optional.string().$; + if (valueError) return rej('invalid value param'); + + const set = {}; + if (data) { + Object.entries(data).forEach(([k, v]) => { + set[`data.${k}`] = v; + }); + } else { + set[`data.${key}`] = value; + } + + await Appdata.update({ + app_id: app._id, + user_id: user._id + }, Object.assign({ + app_id: app._id, + user_id: user._id + }, { + $set: set + }), { + upsert: true + }); + + res(204); +}); diff --git a/src/server/api/endpoints/i/authorized_apps.ts b/src/server/api/endpoints/i/authorized_apps.ts new file mode 100644 index 0000000000..40ce7a68c8 --- /dev/null +++ b/src/server/api/endpoints/i/authorized_apps.ts @@ -0,0 +1,43 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import AccessToken from '../../models/access-token'; +import { pack } from '../../models/app'; + +/** + * Get authorized apps of my account + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'sort' parameter + const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$; + if (sortError) return rej('invalid sort param'); + + // Get tokens + const tokens = await AccessToken + .find({ + user_id: user._id + }, { + limit: limit, + skip: offset, + sort: { + _id: sort == 'asc' ? 1 : -1 + } + }); + + // Serialize + res(await Promise.all(tokens.map(async token => + await pack(token.app_id)))); +}); diff --git a/src/server/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts new file mode 100644 index 0000000000..88fb36b1fb --- /dev/null +++ b/src/server/api/endpoints/i/change_password.ts @@ -0,0 +1,42 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import * as bcrypt from 'bcryptjs'; +import User from '../../models/user'; + +/** + * Change password + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'current_password' parameter + const [currentPassword, currentPasswordErr] = $(params.current_password).string().$; + if (currentPasswordErr) return rej('invalid current_password param'); + + // Get 'new_password' parameter + const [newPassword, newPasswordErr] = $(params.new_password).string().$; + if (newPasswordErr) return rej('invalid new_password param'); + + // Compare password + const same = await bcrypt.compare(currentPassword, user.account.password); + + if (!same) { + return rej('incorrect password'); + } + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(newPassword, salt); + + await User.update(user._id, { + $set: { + 'account.password': hash + } + }); + + res(); +}); diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts new file mode 100644 index 0000000000..eb464cf0f0 --- /dev/null +++ b/src/server/api/endpoints/i/favorites.ts @@ -0,0 +1,44 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Favorite from '../../models/favorite'; +import { pack } from '../../models/post'; + +/** + * Get followers of a user + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'sort' parameter + const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$; + if (sortError) return rej('invalid sort param'); + + // Get favorites + const favorites = await Favorite + .find({ + user_id: user._id + }, { + limit: limit, + skip: offset, + sort: { + _id: sort == 'asc' ? 1 : -1 + } + }); + + // Serialize + res(await Promise.all(favorites.map(async favorite => + await pack(favorite.post) + ))); +}); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts new file mode 100644 index 0000000000..688039a0dd --- /dev/null +++ b/src/server/api/endpoints/i/notifications.ts @@ -0,0 +1,110 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Notification from '../../models/notification'; +import Mute from '../../models/mute'; +import { pack } from '../../models/notification'; +import getFriends from '../../common/get-friends'; +import read from '../../common/read-notification'; + +/** + * Get notifications + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'following' parameter + const [following = false, followingError] = + $(params.following).optional.boolean().$; + if (followingError) return rej('invalid following param'); + + // Get 'mark_as_read' parameter + const [markAsRead = true, markAsReadErr] = $(params.mark_as_read).optional.boolean().$; + if (markAsReadErr) return rej('invalid mark_as_read param'); + + // Get 'type' parameter + const [type, typeErr] = $(params.type).optional.array('string').unique().$; + if (typeErr) return rej('invalid type param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + const mute = await Mute.find({ + muter_id: user._id, + deleted_at: { $exists: false } + }); + + const query = { + notifiee_id: user._id, + $and: [{ + notifier_id: { + $nin: mute.map(m => m.mutee_id) + } + }] + } as any; + + const sort = { + _id: -1 + }; + + if (following) { + // ID list of the user itself and other users who the user follows + const followingIds = await getFriends(user._id); + + query.$and.push({ + notifier_id: { + $in: followingIds + } + }); + } + + if (type) { + query.type = { + $in: type + }; + } + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const notifications = await Notification + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(notifications.map(async notification => + await pack(notification)))); + + // Mark as read all + if (notifications.length > 0 && markAsRead) { + read(user._id, notifications); + } +}); diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts new file mode 100644 index 0000000000..ff546fc2bd --- /dev/null +++ b/src/server/api/endpoints/i/pin.ts @@ -0,0 +1,44 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../models/user'; +import Post from '../../models/post'; +import { pack } from '../../models/user'; + +/** + * Pin post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Fetch pinee + const post = await Post.findOne({ + _id: postId, + user_id: user._id + }); + + if (post === null) { + return rej('post not found'); + } + + await User.update(user._id, { + $set: { + pinned_post_id: post._id + } + }); + + // Serialize + const iObj = await pack(user, user, { + detail: true + }); + + // Send response + res(iObj); +}); diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts new file mode 100644 index 0000000000..9ac7b55071 --- /dev/null +++ b/src/server/api/endpoints/i/regenerate_token.ts @@ -0,0 +1,42 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import * as bcrypt from 'bcryptjs'; +import User from '../../models/user'; +import event from '../../event'; +import generateUserToken from '../../common/generate-native-user-token'; + +/** + * Regenerate native token + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'password' parameter + const [password, passwordErr] = $(params.password).string().$; + if (passwordErr) return rej('invalid password param'); + + // Compare password + const same = await bcrypt.compare(password, user.account.password); + + if (!same) { + return rej('incorrect password'); + } + + // Generate secret + const secret = generateUserToken(); + + await User.update(user._id, { + $set: { + 'account.token': secret + } + }); + + res(); + + // Publish event + event(user._id, 'my_token_regenerated'); +}); diff --git a/src/server/api/endpoints/i/signin_history.ts b/src/server/api/endpoints/i/signin_history.ts new file mode 100644 index 0000000000..859e81653d --- /dev/null +++ b/src/server/api/endpoints/i/signin_history.ts @@ -0,0 +1,61 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Signin, { pack } from '../../models/signin'; + +/** + * Get signin history of my account + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + const query = { + user_id: user._id + } as any; + + const sort = { + _id: -1 + }; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const history = await Signin + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(history.map(async record => + await pack(record)))); +}); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts new file mode 100644 index 0000000000..3d52de2cc5 --- /dev/null +++ b/src/server/api/endpoints/i/update.ts @@ -0,0 +1,97 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../models/user'; +import event from '../../event'; +import config from '../../../../conf'; + +/** + * Update myself + * + * @param {any} params + * @param {any} user + * @param {any} _ + * @param {boolean} isSecure + * @return {Promise} + */ +module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => { + // Get 'name' parameter + const [name, nameErr] = $(params.name).optional.string().pipe(isValidName).$; + if (nameErr) return rej('invalid name param'); + if (name) user.name = name; + + // Get 'description' parameter + const [description, descriptionErr] = $(params.description).optional.nullable.string().pipe(isValidDescription).$; + if (descriptionErr) return rej('invalid description param'); + if (description !== undefined) user.description = description; + + // 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; + + // 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; + + // Get 'avatar_id' parameter + const [avatarId, avatarIdErr] = $(params.avatar_id).optional.id().$; + if (avatarIdErr) return rej('invalid avatar_id param'); + if (avatarId) user.avatar_id = avatarId; + + // Get 'banner_id' parameter + const [bannerId, bannerIdErr] = $(params.banner_id).optional.id().$; + if (bannerIdErr) return rej('invalid banner_id param'); + if (bannerId) user.banner_id = bannerId; + + // Get 'is_bot' parameter + const [isBot, isBotErr] = $(params.is_bot).optional.boolean().$; + if (isBotErr) return rej('invalid is_bot param'); + if (isBot != null) user.account.is_bot = isBot; + + // Get 'auto_watch' parameter + const [autoWatch, autoWatchErr] = $(params.auto_watch).optional.boolean().$; + if (autoWatchErr) return rej('invalid auto_watch param'); + if (autoWatch != null) user.account.settings.auto_watch = autoWatch; + + await User.update(user._id, { + $set: { + name: user.name, + description: user.description, + avatar_id: user.avatar_id, + banner_id: user.banner_id, + 'account.profile': user.account.profile, + 'account.is_bot': user.account.is_bot, + 'account.settings': user.account.settings + } + }); + + // Serialize + const iObj = await pack(user, user, { + detail: true, + includeSecrets: isSecure + }); + + // Send response + res(iObj); + + // Publish i updated event + event(user._id, 'i_updated', iObj); + + // Update search index + if (config.elasticsearch.enable) { + const es = require('../../../db/elasticsearch'); + + es.index({ + index: 'misskey', + type: 'user', + id: user._id.toString(), + body: { + name: user.name, + bio: user.bio + } + }); + } +}); diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts new file mode 100644 index 0000000000..c772ed5dc3 --- /dev/null +++ b/src/server/api/endpoints/i/update_client_setting.ts @@ -0,0 +1,43 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { pack } from '../../models/user'; +import event from '../../event'; + +/** + * Update myself + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'name' parameter + const [name, nameErr] = $(params.name).string().$; + if (nameErr) return rej('invalid name param'); + + // Get 'value' parameter + const [value, valueErr] = $(params.value).nullable.any().$; + if (valueErr) return rej('invalid value param'); + + const x = {}; + x[`account.client_settings.${name}`] = value; + + await User.update(user._id, { + $set: x + }); + + // Serialize + user.account.client_settings[name] = value; + const iObj = await pack(user, user, { + detail: true, + includeSecrets: true + }); + + // Send response + res(iObj); + + // Publish i updated event + event(user._id, 'i_updated', iObj); +}); diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts new file mode 100644 index 0000000000..9ce44e25ee --- /dev/null +++ b/src/server/api/endpoints/i/update_home.ts @@ -0,0 +1,60 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../models/user'; +import event from '../../event'; + +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'home' parameter + const [home, homeErr] = $(params.home).optional.array().each( + $().strict.object() + .have('name', $().string()) + .have('id', $().string()) + .have('place', $().string()) + .have('data', $().object())).$; + if (homeErr) return rej('invalid home param'); + + // Get 'id' parameter + const [id, idErr] = $(params.id).optional.string().$; + if (idErr) return rej('invalid id param'); + + // Get 'data' parameter + const [data, dataErr] = $(params.data).optional.object().$; + if (dataErr) return rej('invalid data param'); + + if (home) { + await User.update(user._id, { + $set: { + 'account.client_settings.home': home + } + }); + + res(); + + event(user._id, 'home_updated', { + home + }); + } else { + if (id == null && data == null) return rej('you need to set id and data params if home param unset'); + + const _home = user.account.client_settings.home; + const widget = _home.find(w => w.id == id); + + if (widget == null) return rej('widget not found'); + + widget.data = data; + + await User.update(user._id, { + $set: { + 'account.client_settings.home': _home + } + }); + + res(); + + event(user._id, 'home_updated', { + id, data + }); + } +}); diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts new file mode 100644 index 0000000000..1daddf42b9 --- /dev/null +++ b/src/server/api/endpoints/i/update_mobile_home.ts @@ -0,0 +1,59 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../models/user'; +import event from '../../event'; + +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'home' parameter + const [home, homeErr] = $(params.home).optional.array().each( + $().strict.object() + .have('name', $().string()) + .have('id', $().string()) + .have('data', $().object())).$; + if (homeErr) return rej('invalid home param'); + + // Get 'id' parameter + const [id, idErr] = $(params.id).optional.string().$; + if (idErr) return rej('invalid id param'); + + // Get 'data' parameter + const [data, dataErr] = $(params.data).optional.object().$; + if (dataErr) return rej('invalid data param'); + + if (home) { + await User.update(user._id, { + $set: { + 'account.client_settings.mobile_home': home + } + }); + + res(); + + event(user._id, 'mobile_home_updated', { + home + }); + } else { + if (id == null && data == null) return rej('you need to set id and data params if home param unset'); + + const _home = user.account.client_settings.mobile_home || []; + const widget = _home.find(w => w.id == id); + + if (widget == null) return rej('widget not found'); + + widget.data = data; + + await User.update(user._id, { + $set: { + 'account.client_settings.mobile_home': _home + } + }); + + res(); + + event(user._id, 'mobile_home_updated', { + id, data + }); + } +}); diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts new file mode 100644 index 0000000000..1683ca7a89 --- /dev/null +++ b/src/server/api/endpoints/messaging/history.ts @@ -0,0 +1,43 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import History from '../../models/messaging-history'; +import Mute from '../../models/mute'; +import { pack } from '../../models/messaging-message'; + +/** + * Show messaging history + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + const mute = await Mute.find({ + muter_id: user._id, + deleted_at: { $exists: false } + }); + + // Get history + const history = await History + .find({ + user_id: user._id, + partner: { + $nin: mute.map(m => m.mutee_id) + } + }, { + limit: limit, + sort: { + updated_at: -1 + } + }); + + // Serialize + res(await Promise.all(history.map(async h => + await pack(h.message, user)))); +}); diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts new file mode 100644 index 0000000000..67ba5e9d6d --- /dev/null +++ b/src/server/api/endpoints/messaging/messages.ts @@ -0,0 +1,102 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Message from '../../models/messaging-message'; +import User from '../../models/user'; +import { pack } from '../../models/messaging-message'; +import read from '../../common/read-messaging-message'; + +/** + * Get messages + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [recipientId, recipientIdErr] = $(params.user_id).id().$; + if (recipientIdErr) return rej('invalid user_id param'); + + // Fetch recipient + const recipient = await User.findOne({ + _id: recipientId + }, { + fields: { + _id: true + } + }); + + if (recipient === null) { + return rej('user not found'); + } + + // Get 'mark_as_read' parameter + const [markAsRead = true, markAsReadErr] = $(params.mark_as_read).optional.boolean().$; + if (markAsReadErr) return rej('invalid mark_as_read param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + const query = { + $or: [{ + user_id: user._id, + recipient_id: recipient._id + }, { + user_id: recipient._id, + recipient_id: user._id + }] + } as any; + + const sort = { + _id: -1 + }; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const messages = await Message + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(messages.map(async message => + await pack(message, user, { + populateRecipient: false + })))); + + if (messages.length === 0) { + return; + } + + // Mark as read all + if (markAsRead) { + read(user._id, recipient._id, messages); + } +}); diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts new file mode 100644 index 0000000000..5184b2bd34 --- /dev/null +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -0,0 +1,156 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Message from '../../../models/messaging-message'; +import { isValidText } from '../../../models/messaging-message'; +import History from '../../../models/messaging-history'; +import User from '../../../models/user'; +import Mute from '../../../models/mute'; +import DriveFile from '../../../models/drive-file'; +import { pack } from '../../../models/messaging-message'; +import publishUserStream from '../../../event'; +import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event'; +import config from '../../../../../conf'; + +/** + * Create a message + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [recipientId, recipientIdErr] = $(params.user_id).id().$; + if (recipientIdErr) return rej('invalid user_id param'); + + // Myself + if (recipientId.equals(user._id)) { + return rej('cannot send message to myself'); + } + + // Fetch recipient + const recipient = await User.findOne({ + _id: recipientId + }, { + fields: { + _id: true + } + }); + + if (recipient === null) { + return rej('user not found'); + } + + // Get 'text' parameter + const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; + if (textErr) return rej('invalid text'); + + // Get 'file_id' parameter + const [fileId, fileIdErr] = $(params.file_id).optional.id().$; + if (fileIdErr) return rej('invalid file_id param'); + + let file = null; + if (fileId !== undefined) { + file = await DriveFile.findOne({ + _id: fileId, + 'metadata.user_id': user._id + }); + + if (file === null) { + return rej('file not found'); + } + } + + // テキストが無いかつ添付ファイルも無かったらエラー + if (text === undefined && file === null) { + return rej('text or file is required'); + } + + // メッセージを作成 + const message = await Message.insert({ + created_at: new Date(), + file_id: file ? file._id : undefined, + recipient_id: recipient._id, + text: text ? text : undefined, + user_id: user._id, + is_read: false + }); + + // Serialize + const messageObj = await pack(message); + + // Reponse + res(messageObj); + + // 自分のストリーム + publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj); + publishMessagingIndexStream(message.user_id, 'message', messageObj); + publishUserStream(message.user_id, 'messaging_message', messageObj); + + // 相手のストリーム + publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj); + publishMessagingIndexStream(message.recipient_id, 'message', messageObj); + publishUserStream(message.recipient_id, 'messaging_message', messageObj); + + // 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する + setTimeout(async () => { + const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true }); + if (!freshMessage.is_read) { + //#region ただしミュートされているなら発行しない + const mute = await Mute.find({ + muter_id: recipient._id, + deleted_at: { $exists: false } + }); + const mutedUserIds = mute.map(m => m.mutee_id.toString()); + if (mutedUserIds.indexOf(user._id.toString()) != -1) { + return; + } + //#endregion + + publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj); + pushSw(message.recipient_id, 'unread_messaging_message', messageObj); + } + }, 3000); + + // Register to search database + if (message.text && config.elasticsearch.enable) { + const es = require('../../../db/elasticsearch'); + + es.index({ + index: 'misskey', + type: 'messaging_message', + id: message._id.toString(), + body: { + text: message.text + } + }); + } + + // 履歴作成(自分) + History.update({ + user_id: user._id, + partner: recipient._id + }, { + updated_at: new Date(), + user_id: user._id, + partner: recipient._id, + message: message._id + }, { + upsert: true + }); + + // 履歴作成(相手) + History.update({ + user_id: recipient._id, + partner: user._id + }, { + updated_at: new Date(), + user_id: recipient._id, + partner: user._id, + message: message._id + }, { + upsert: true + }); +}); diff --git a/src/server/api/endpoints/messaging/unread.ts b/src/server/api/endpoints/messaging/unread.ts new file mode 100644 index 0000000000..c4326e1d22 --- /dev/null +++ b/src/server/api/endpoints/messaging/unread.ts @@ -0,0 +1,33 @@ +/** + * Module dependencies + */ +import Message from '../../models/messaging-message'; +import Mute from '../../models/mute'; + +/** + * Get count of unread messages + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const mute = await Mute.find({ + muter_id: user._id, + deleted_at: { $exists: false } + }); + const mutedUserIds = mute.map(m => m.mutee_id); + + const count = await Message + .count({ + user_id: { + $nin: mutedUserIds + }, + recipient_id: user._id, + is_read: false + }); + + res({ + count: count + }); +}); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts new file mode 100644 index 0000000000..10625ec66f --- /dev/null +++ b/src/server/api/endpoints/meta.ts @@ -0,0 +1,59 @@ +/** + * Module dependencies + */ +import * as os from 'os'; +import version from '../../../version'; +import config from '../../../conf'; +import Meta from '../models/meta'; + +/** + * @swagger + * /meta: + * post: + * summary: Show the misskey's information + * responses: + * 200: + * description: Success + * schema: + * type: object + * properties: + * maintainer: + * description: maintainer's name + * type: string + * commit: + * description: latest commit's hash + * type: string + * secure: + * description: whether the server supports secure protocols + * type: boolean + * + * default: + * description: Failed + * schema: + * $ref: "#/definitions/Error" + */ + +/** + * Show core info + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + const meta = (await Meta.findOne()) || {}; + + res({ + maintainer: config.maintainer, + version: version, + secure: config.https != null, + machine: os.hostname(), + os: os.platform(), + node: process.version, + cpu: { + model: os.cpus()[0].model, + cores: os.cpus().length + }, + top_image: meta.top_image, + broadcasts: meta.broadcasts + }); +}); diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts new file mode 100644 index 0000000000..f99b40d32e --- /dev/null +++ b/src/server/api/endpoints/mute/create.ts @@ -0,0 +1,61 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../models/user'; +import Mute from '../../models/mute'; + +/** + * Mute a user + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const muter = user; + + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // 自分自身 + if (user._id.equals(userId)) { + return rej('mutee is yourself'); + } + + // Get mutee + const mutee = await User.findOne({ + _id: userId + }, { + fields: { + data: false, + 'account.profile': false + } + }); + + if (mutee === null) { + return rej('user not found'); + } + + // Check if already muting + const exist = await Mute.findOne({ + muter_id: muter._id, + mutee_id: mutee._id, + deleted_at: { $exists: false } + }); + + if (exist !== null) { + return rej('already muting'); + } + + // Create mute + await Mute.insert({ + created_at: new Date(), + muter_id: muter._id, + mutee_id: mutee._id, + }); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts new file mode 100644 index 0000000000..36e2fd101a --- /dev/null +++ b/src/server/api/endpoints/mute/delete.ts @@ -0,0 +1,63 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../models/user'; +import Mute from '../../models/mute'; + +/** + * Unmute a user + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const muter = user; + + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Check if the mutee is yourself + if (user._id.equals(userId)) { + return rej('mutee is yourself'); + } + + // Get mutee + const mutee = await User.findOne({ + _id: userId + }, { + fields: { + data: false, + 'account.profile': false + } + }); + + if (mutee === null) { + return rej('user not found'); + } + + // Check not muting + const exist = await Mute.findOne({ + muter_id: muter._id, + mutee_id: mutee._id, + deleted_at: { $exists: false } + }); + + if (exist === null) { + return rej('already not muting'); + } + + // Delete mute + await Mute.update({ + _id: exist._id + }, { + $set: { + deleted_at: new Date() + } + }); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts new file mode 100644 index 0000000000..19e3b157e6 --- /dev/null +++ b/src/server/api/endpoints/mute/list.ts @@ -0,0 +1,73 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Mute from '../../models/mute'; +import { pack } from '../../models/user'; +import getFriends from '../../common/get-friends'; + +/** + * Get muted users of a user + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'iknow' parameter + const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$; + if (iknowErr) return rej('invalid iknow param'); + + // Get 'limit' parameter + const [limit = 30, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'cursor' parameter + const [cursor = null, cursorErr] = $(params.cursor).optional.id().$; + if (cursorErr) return rej('invalid cursor param'); + + // Construct query + const query = { + muter_id: me._id, + deleted_at: { $exists: false } + } as any; + + if (iknow) { + // Get my friends + const myFriends = await getFriends(me._id); + + query.mutee_id = { + $in: myFriends + }; + } + + // カーソルが指定されている場合 + if (cursor) { + query._id = { + $lt: cursor + }; + } + + // Get mutes + const mutes = await Mute + .find(query, { + limit: limit + 1, + sort: { _id: -1 } + }); + + // 「次のページ」があるかどうか + const inStock = mutes.length === limit + 1; + if (inStock) { + mutes.pop(); + } + + // Serialize + const users = await Promise.all(mutes.map(async m => + await pack(m.mutee_id, me, { detail: true }))); + + // Response + res({ + users: users, + next: inStock ? mutes[mutes.length - 1]._id : null, + }); +}); diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts new file mode 100644 index 0000000000..b236190506 --- /dev/null +++ b/src/server/api/endpoints/my/apps.ts @@ -0,0 +1,40 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import App, { pack } from '../../models/app'; + +/** + * Get my apps + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + const query = { + user_id: user._id + }; + + // Execute query + const apps = await App + .find(query, { + limit: limit, + skip: offset, + sort: { + _id: -1 + } + }); + + // Reply + res(await Promise.all(apps.map(async app => + await pack(app)))); +}); diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts new file mode 100644 index 0000000000..845d6b29ce --- /dev/null +++ b/src/server/api/endpoints/notifications/get_unread_count.ts @@ -0,0 +1,33 @@ +/** + * Module dependencies + */ +import Notification from '../../models/notification'; +import Mute from '../../models/mute'; + +/** + * Get count of unread notifications + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const mute = await Mute.find({ + muter_id: user._id, + deleted_at: { $exists: false } + }); + const mutedUserIds = mute.map(m => m.mutee_id); + + const count = await Notification + .count({ + notifiee_id: user._id, + notifier_id: { + $nin: mutedUserIds + }, + is_read: false + }); + + res({ + count: count + }); +}); diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts new file mode 100644 index 0000000000..3550e344c4 --- /dev/null +++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts @@ -0,0 +1,32 @@ +/** + * Module dependencies + */ +import Notification from '../../models/notification'; +import event from '../../event'; + +/** + * Mark as read all notifications + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Update documents + await Notification.update({ + notifiee_id: user._id, + is_read: false + }, { + $set: { + is_read: true + } + }, { + multi: true + }); + + // Response + res(); + + // 全ての通知を読みましたよというイベントを発行 + event(user._id, 'read_all_notifications'); +}); diff --git a/src/server/api/endpoints/othello/games.ts b/src/server/api/endpoints/othello/games.ts new file mode 100644 index 0000000000..2a6bbb4043 --- /dev/null +++ b/src/server/api/endpoints/othello/games.ts @@ -0,0 +1,62 @@ +import $ from 'cafy'; +import Game, { pack } from '../../models/othello-game'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'my' parameter + const [my = false, myErr] = $(params.my).optional.boolean().$; + if (myErr) return rej('invalid my param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + const q: any = my ? { + is_started: true, + $or: [{ + user1_id: user._id + }, { + user2_id: user._id + }] + } : { + is_started: true + }; + + const sort = { + _id: -1 + }; + + if (sinceId) { + sort._id = 1; + q._id = { + $gt: sinceId + }; + } else if (untilId) { + q._id = { + $lt: untilId + }; + } + + // Fetch games + const games = await Game.find(q, { + sort, + limit + }); + + // Reponse + res(Promise.all(games.map(async (g) => await pack(g, user, { + detail: false + })))); +}); diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts new file mode 100644 index 0000000000..2b0db4dd00 --- /dev/null +++ b/src/server/api/endpoints/othello/games/show.ts @@ -0,0 +1,32 @@ +import $ from 'cafy'; +import Game, { pack } from '../../../models/othello-game'; +import Othello from '../../../../common/othello/core'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'game_id' parameter + const [gameId, gameIdErr] = $(params.game_id).id().$; + if (gameIdErr) return rej('invalid game_id param'); + + const game = await Game.findOne({ _id: gameId }); + + if (game == null) { + return rej('game not found'); + } + + const o = new Othello(game.settings.map, { + isLlotheo: game.settings.is_llotheo, + canPutEverywhere: game.settings.can_put_everywhere, + loopedBoard: game.settings.looped_board + }); + + game.logs.forEach(log => { + o.put(log.color, log.pos); + }); + + const packed = await pack(game, user); + + res(Object.assign({ + board: o.board, + turn: o.turn + }, packed)); +}); diff --git a/src/server/api/endpoints/othello/invitations.ts b/src/server/api/endpoints/othello/invitations.ts new file mode 100644 index 0000000000..02fb421fbc --- /dev/null +++ b/src/server/api/endpoints/othello/invitations.ts @@ -0,0 +1,15 @@ +import Matching, { pack as packMatching } from '../../models/othello-matching'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Find session + const invitations = await Matching.find({ + child_id: user._id + }, { + sort: { + _id: -1 + } + }); + + // Reponse + res(Promise.all(invitations.map(async (i) => await packMatching(i, user)))); +}); diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts new file mode 100644 index 0000000000..b73e105ef0 --- /dev/null +++ b/src/server/api/endpoints/othello/match.ts @@ -0,0 +1,95 @@ +import $ from 'cafy'; +import Matching, { pack as packMatching } from '../../models/othello-matching'; +import Game, { pack as packGame } from '../../models/othello-game'; +import User from '../../models/user'; +import publishUserStream, { publishOthelloStream } from '../../event'; +import { eighteight } from '../../../common/othello/maps'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [childId, childIdErr] = $(params.user_id).id().$; + if (childIdErr) return rej('invalid user_id param'); + + // Myself + if (childId.equals(user._id)) { + return rej('invalid user_id param'); + } + + // Find session + const exist = await Matching.findOne({ + parent_id: childId, + child_id: user._id + }); + + if (exist) { + // Destroy session + Matching.remove({ + _id: exist._id + }); + + // Create game + const game = await Game.insert({ + created_at: new Date(), + user1_id: exist.parent_id, + user2_id: user._id, + user1_accepted: false, + user2_accepted: false, + is_started: false, + is_ended: false, + logs: [], + settings: { + map: eighteight.data, + bw: 'random', + is_llotheo: false + } + }); + + // Reponse + res(await packGame(game, user)); + + publishOthelloStream(exist.parent_id, 'matched', await packGame(game, exist.parent_id)); + + const other = await Matching.count({ + child_id: user._id + }); + + if (other == 0) { + publishUserStream(user._id, 'othello_no_invites'); + } + } else { + // Fetch child + const child = await User.findOne({ + _id: childId + }, { + fields: { + _id: true + } + }); + + if (child === null) { + return rej('user not found'); + } + + // 以前のセッションはすべて削除しておく + await Matching.remove({ + parent_id: user._id + }); + + // セッションを作成 + const matching = await Matching.insert({ + created_at: new Date(), + parent_id: user._id, + child_id: child._id + }); + + // Reponse + res(); + + const packed = await packMatching(matching, child); + + // 招待 + publishOthelloStream(child._id, 'invited', packed); + + publishUserStream(child._id, 'othello_invited', packed); + } +}); diff --git a/src/server/api/endpoints/othello/match/cancel.ts b/src/server/api/endpoints/othello/match/cancel.ts new file mode 100644 index 0000000000..6f751ef835 --- /dev/null +++ b/src/server/api/endpoints/othello/match/cancel.ts @@ -0,0 +1,9 @@ +import Matching from '../../../models/othello-matching'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + await Matching.remove({ + parent_id: user._id + }); + + res(); +}); diff --git a/src/server/api/endpoints/posts.ts b/src/server/api/endpoints/posts.ts new file mode 100644 index 0000000000..7df744d2a3 --- /dev/null +++ b/src/server/api/endpoints/posts.ts @@ -0,0 +1,97 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post, { pack } from '../models/post'; + +/** + * Lists all posts + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'reply' parameter + const [reply, replyErr] = $(params.reply).optional.boolean().$; + if (replyErr) return rej('invalid reply param'); + + // Get 'repost' parameter + const [repost, repostErr] = $(params.repost).optional.boolean().$; + if (repostErr) return rej('invalid repost param'); + + // Get 'media' parameter + const [media, mediaErr] = $(params.media).optional.boolean().$; + if (mediaErr) return rej('invalid media param'); + + // Get 'poll' parameter + const [poll, pollErr] = $(params.poll).optional.boolean().$; + if (pollErr) return rej('invalid poll param'); + + // Get 'bot' parameter + //const [bot, botErr] = $(params.bot).optional.boolean().$; + //if (botErr) return rej('invalid bot param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + // Construct query + const sort = { + _id: -1 + }; + const query = {} as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + if (reply != undefined) { + query.reply_id = reply ? { $exists: true, $ne: null } : null; + } + + if (repost != undefined) { + query.repost_id = repost ? { $exists: true, $ne: null } : null; + } + + if (media != undefined) { + query.media_ids = media ? { $exists: true, $ne: null } : null; + } + + if (poll != undefined) { + query.poll = poll ? { $exists: true, $ne: null } : null; + } + + // TODO + //if (bot != undefined) { + // query.is_bot = bot; + //} + + // Issue query + const posts = await Post + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(posts.map(async post => await pack(post)))); +}); diff --git a/src/server/api/endpoints/posts/categorize.ts b/src/server/api/endpoints/posts/categorize.ts new file mode 100644 index 0000000000..0c85c2b4e0 --- /dev/null +++ b/src/server/api/endpoints/posts/categorize.ts @@ -0,0 +1,52 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post from '../../models/post'; + +/** + * Categorize a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + if (!user.account.is_pro) { + return rej('This endpoint is available only from a Pro account'); + } + + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Get categorizee + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + if (post.is_category_verified) { + return rej('This post already has the verified category'); + } + + // Get 'category' parameter + const [category, categoryErr] = $(params.category).string().or([ + 'music', 'game', 'anime', 'it', 'gadgets', 'photography' + ]).$; + if (categoryErr) return rej('invalid category param'); + + // Set category + Post.update({ _id: post._id }, { + $set: { + category: category, + is_category_verified: true + } + }); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/posts/context.ts b/src/server/api/endpoints/posts/context.ts new file mode 100644 index 0000000000..5ba3758975 --- /dev/null +++ b/src/server/api/endpoints/posts/context.ts @@ -0,0 +1,63 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post, { pack } from '../../models/post'; + +/** + * Show a context of a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Lookup post + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + const context = []; + let i = 0; + + async function get(id) { + i++; + const p = await Post.findOne({ _id: id }); + + if (i > offset) { + context.push(p); + } + + if (context.length == limit) { + return; + } + + if (p.reply_id) { + await get(p.reply_id); + } + } + + if (post.reply_id) { + await get(post.reply_id); + } + + // Serialize + res(await Promise.all(context.map(async post => + await pack(post, user)))); +}); diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts new file mode 100644 index 0000000000..bc9af843b6 --- /dev/null +++ b/src/server/api/endpoints/posts/create.ts @@ -0,0 +1,536 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import deepEqual = require('deep-equal'); +import parse from '../../common/text'; +import { default as Post, IPost, isValidText } from '../../models/post'; +import { default as User, ILocalAccount, IUser } from '../../models/user'; +import { default as Channel, IChannel } from '../../models/channel'; +import Following from '../../models/following'; +import Mute from '../../models/mute'; +import DriveFile from '../../models/drive-file'; +import Watching from '../../models/post-watching'; +import ChannelWatching from '../../models/channel-watching'; +import { pack } from '../../models/post'; +import notify from '../../common/notify'; +import watch from '../../common/watch-post'; +import event, { pushSw, publishChannelStream } from '../../event'; +import getAcct from '../../../common/user/get-acct'; +import parseAcct from '../../../common/user/parse-acct'; +import config from '../../../../conf'; + +/** + * Create a post + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise} + */ +module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { + // Get 'text' parameter + const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; + if (textErr) return rej('invalid text'); + + // Get 'via_mobile' parameter + const [viaMobile = false, viaMobileErr] = $(params.via_mobile).optional.boolean().$; + if (viaMobileErr) return rej('invalid via_mobile'); + + // Get 'tags' parameter + const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$; + if (tagsErr) return rej('invalid tags'); + + // Get 'geo' parameter + const [geo, geoErr] = $(params.geo).optional.nullable.strict.object() + .have('latitude', $().number().range(-90, 90)) + .have('longitude', $().number().range(-180, 180)) + .have('altitude', $().nullable.number()) + .have('accuracy', $().nullable.number()) + .have('altitudeAccuracy', $().nullable.number()) + .have('heading', $().nullable.number().range(0, 360)) + .have('speed', $().nullable.number()) + .$; + if (geoErr) return rej('invalid geo'); + + // Get 'media_ids' parameter + const [mediaIds, mediaIdsErr] = $(params.media_ids).optional.array('id').unique().range(1, 4).$; + if (mediaIdsErr) return rej('invalid media_ids'); + + let files = []; + if (mediaIds !== undefined) { + // Fetch files + // forEach だと途中でエラーなどがあっても return できないので + // 敢えて for を使っています。 + for (const mediaId of mediaIds) { + // Fetch file + // SELECT _id + const entity = await DriveFile.findOne({ + _id: mediaId, + 'metadata.user_id': user._id + }); + + if (entity === null) { + return rej('file not found'); + } else { + files.push(entity); + } + } + } else { + files = null; + } + + // Get 'repost_id' parameter + const [repostId, repostIdErr] = $(params.repost_id).optional.id().$; + if (repostIdErr) return rej('invalid repost_id'); + + let repost: IPost = null; + let isQuote = false; + if (repostId !== undefined) { + // Fetch repost to post + repost = await Post.findOne({ + _id: repostId + }); + + if (repost == null) { + return rej('repostee is not found'); + } else if (repost.repost_id && !repost.text && !repost.media_ids) { + return rej('cannot repost to repost'); + } + + // Fetch recently post + const latestPost = await Post.findOne({ + user_id: user._id + }, { + sort: { + _id: -1 + } + }); + + isQuote = text != null || files != null; + + // 直近と同じRepost対象かつ引用じゃなかったらエラー + if (latestPost && + latestPost.repost_id && + latestPost.repost_id.equals(repost._id) && + !isQuote) { + return rej('cannot repost same post that already reposted in your latest post'); + } + + // 直近がRepost対象かつ引用じゃなかったらエラー + if (latestPost && + latestPost._id.equals(repost._id) && + !isQuote) { + return rej('cannot repost your latest post'); + } + } + + // Get 'reply_id' parameter + const [replyId, replyIdErr] = $(params.reply_id).optional.id().$; + if (replyIdErr) return rej('invalid reply_id'); + + let reply: IPost = null; + if (replyId !== undefined) { + // Fetch reply + reply = await Post.findOne({ + _id: replyId + }); + + if (reply === null) { + return rej('in reply to post is not found'); + } + + // 返信対象が引用でないRepostだったらエラー + if (reply.repost_id && !reply.text && !reply.media_ids) { + return rej('cannot reply to repost'); + } + } + + // Get 'channel_id' parameter + const [channelId, channelIdErr] = $(params.channel_id).optional.id().$; + if (channelIdErr) return rej('invalid channel_id'); + + let channel: IChannel = null; + if (channelId !== undefined) { + // Fetch channel + channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + // 返信対象の投稿がこのチャンネルじゃなかったらダメ + if (reply && !channelId.equals(reply.channel_id)) { + return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません'); + } + + // Repost対象の投稿がこのチャンネルじゃなかったらダメ + if (repost && !channelId.equals(repost.channel_id)) { + return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません'); + } + + // 引用ではないRepostはダメ + if (repost && !isQuote) { + return rej('チャンネル内部では引用ではないRepostをすることはできません'); + } + } else { + // 返信対象の投稿がチャンネルへの投稿だったらダメ + if (reply && reply.channel_id != null) { + return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません'); + } + + // Repost対象の投稿がチャンネルへの投稿だったらダメ + if (repost && repost.channel_id != null) { + return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません'); + } + } + + // Get 'poll' parameter + const [poll, pollErr] = $(params.poll).optional.strict.object() + .have('choices', $().array('string') + .unique() + .range(2, 10) + .each(c => c.length > 0 && c.length < 50)) + .$; + if (pollErr) return rej('invalid poll'); + + if (poll) { + (poll as any).choices = (poll as any).choices.map((choice, i) => ({ + id: i, // IDを付与 + text: choice.trim(), + votes: 0 + })); + } + + // テキストが無いかつ添付ファイルが無いかつRepostも無いかつ投票も無かったらエラー + if (text === undefined && files === null && repost === null && poll === undefined) { + return rej('text, media_ids, repost_id or poll is required'); + } + + // 直近の投稿と重複してたらエラー + // TODO: 直近の投稿が一日前くらいなら重複とは見なさない + if (user.latest_post) { + if (deepEqual({ + text: user.latest_post.text, + reply: user.latest_post.reply_id ? user.latest_post.reply_id.toString() : null, + repost: user.latest_post.repost_id ? user.latest_post.repost_id.toString() : null, + media_ids: (user.latest_post.media_ids || []).map(id => id.toString()) + }, { + text: text, + reply: reply ? reply._id.toString() : null, + repost: repost ? repost._id.toString() : null, + media_ids: (files || []).map(file => file._id.toString()) + })) { + return rej('duplicate'); + } + } + + let tokens = null; + if (text) { + // Analyze + tokens = parse(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 post = await Post.insert({ + created_at: new Date(), + channel_id: channel ? channel._id : undefined, + index: channel ? channel.index + 1 : undefined, + media_ids: files ? files.map(file => file._id) : undefined, + reply_id: reply ? reply._id : undefined, + repost_id: repost ? repost._id : undefined, + poll: poll, + text: text, + tags: tags, + user_id: user._id, + app_id: app ? app._id : null, + via_mobile: viaMobile, + geo, + + // 以下非正規化データ + _reply: reply ? { user_id: reply.user_id } : undefined, + _repost: repost ? { user_id: repost.user_id } : undefined, + }); + + // Serialize + const postObj = await pack(post); + + // Reponse + res({ + created_post: postObj + }); + + //#region Post processes + + User.update({ _id: user._id }, { + $set: { + latest_post: post + } + }); + + 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.mutee_id.toString()); + if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) { + event(mentionee, reason, postObj); + pushSw(mentionee, reason, postObj); + } + } + } + + // タイムラインへの投稿 + if (!channel) { + // Publish event to myself's stream + event(user._id, 'post', postObj); + + // Fetch all followers + const followers = await Following + .find({ + followee_id: user._id, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }, { + follower_id: true, + _id: false + }); + + // Publish event to followers stream + followers.forEach(following => + event(following.follower_id, 'post', postObj)); + } + + // チャンネルへの投稿 + if (channel) { + // Increment channel index(posts count) + Channel.update({ _id: channel._id }, { + $inc: { + index: 1 + } + }); + + // Publish event to channel + publishChannelStream(channel._id, 'post', postObj); + + // Get channel watchers + const watches = await ChannelWatching.find({ + channel_id: channel._id, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }); + + // チャンネルの視聴者(のタイムライン)に配信 + watches.forEach(w => { + event(w.user_id, 'post', postObj); + }); + } + + // Increment my posts count + User.update({ _id: user._id }, { + $inc: { + posts_count: 1 + } + }); + + // If has in reply to post + if (reply) { + // Increment replies count + Post.update({ _id: reply._id }, { + $inc: { + replies_count: 1 + } + }); + + // 自分自身へのリプライでない限りは通知を作成 + notify(reply.user_id, user._id, 'reply', { + post_id: post._id + }); + + // Fetch watchers + Watching + .find({ + post_id: reply._id, + user_id: { $ne: user._id }, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }, { + fields: { + user_id: true + } + }) + .then(watchers => { + watchers.forEach(watcher => { + notify(watcher.user_id, user._id, 'reply', { + post_id: post._id + }); + }); + }); + + // この投稿をWatchする + if ((user.account as ILocalAccount).settings.auto_watch !== false) { + watch(user._id, reply); + } + + // Add mention + addMention(reply.user_id, 'reply'); + } + + // If it is repost + if (repost) { + // Notify + const type = text ? 'quote' : 'repost'; + notify(repost.user_id, user._id, type, { + post_id: post._id + }); + + // Fetch watchers + Watching + .find({ + post_id: repost._id, + user_id: { $ne: user._id }, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }, { + fields: { + user_id: true + } + }) + .then(watchers => { + watchers.forEach(watcher => { + notify(watcher.user_id, user._id, type, { + post_id: post._id + }); + }); + }); + + // この投稿をWatchする + // TODO: ユーザーが「Repostしたときに自動でWatchする」設定を + // オフにしていた場合はしない + watch(user._id, repost); + + // If it is quote repost + if (text) { + // Add mention + addMention(repost.user_id, 'quote'); + } else { + // Publish event + if (!user._id.equals(repost.user_id)) { + event(repost.user_id, 'repost', postObj); + } + } + + // 今までで同じ投稿をRepostしているか + const existRepost = await Post.findOne({ + user_id: user._id, + repost_id: repost._id, + _id: { + $ne: post._id + } + }); + + if (!existRepost) { + // Update repostee status + Post.update({ _id: repost._id }, { + $inc: { + repost_count: 1 + } + }); + } + } + + // If has text content + if (text) { + /* + // Extract a hashtags + const hashtags = tokens + .filter(t => t.type == 'hashtag') + .map(t => t.hashtag) + // Drop dupulicates + .filter((v, i, s) => s.indexOf(v) == i); + + // ハッシュタグをデータベースに登録 + registerHashtags(user, hashtags); + */ + // Extract an '@' mentions + const atMentions = tokens + .filter(t => t.type == 'mention') + .map(getAcct) + // 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(parseAcct(mention), { _id: true }); + + // When mentioned user not found + if (mentionee == null) return; + + // 既に言及されたユーザーに対する返信や引用repostの場合も無視 + if (reply && reply.user_id.equals(mentionee._id)) return; + if (repost && repost.user_id.equals(mentionee._id)) return; + + // Add mention + addMention(mentionee._id, 'mention'); + + // Create notification + notify(mentionee._id, user._id, 'mention', { + post_id: post._id + }); + + return; + })); + } + + // Register to search database + if (text && config.elasticsearch.enable) { + const es = require('../../../db/elasticsearch'); + + es.index({ + index: 'misskey', + type: 'post', + id: post._id.toString(), + body: { + text: post.text + } + }); + } + + // Append mentions data + if (mentions.length > 0) { + Post.update({ _id: post._id }, { + $set: { + mentions: mentions + } + }); + } + + //#endregion +}); diff --git a/src/server/api/endpoints/posts/favorites/create.ts b/src/server/api/endpoints/posts/favorites/create.ts new file mode 100644 index 0000000000..f9dee271b5 --- /dev/null +++ b/src/server/api/endpoints/posts/favorites/create.ts @@ -0,0 +1,48 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Favorite from '../../../models/favorite'; +import Post from '../../../models/post'; + +/** + * Favorite a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Get favoritee + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + // if already favorited + const exist = await Favorite.findOne({ + post_id: post._id, + user_id: user._id + }); + + if (exist !== null) { + return rej('already favorited'); + } + + // Create favorite + await Favorite.insert({ + created_at: new Date(), + post_id: post._id, + user_id: user._id + }); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/posts/favorites/delete.ts b/src/server/api/endpoints/posts/favorites/delete.ts new file mode 100644 index 0000000000..c4fe7d3234 --- /dev/null +++ b/src/server/api/endpoints/posts/favorites/delete.ts @@ -0,0 +1,46 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Favorite from '../../../models/favorite'; +import Post from '../../../models/post'; + +/** + * Unfavorite a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Get favoritee + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + // if already favorited + const exist = await Favorite.findOne({ + post_id: post._id, + user_id: user._id + }); + + if (exist === null) { + return rej('already not favorited'); + } + + // Delete favorite + await Favorite.deleteOne({ + _id: exist._id + }); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/posts/mentions.ts b/src/server/api/endpoints/posts/mentions.ts new file mode 100644 index 0000000000..7127db0ad1 --- /dev/null +++ b/src/server/api/endpoints/posts/mentions.ts @@ -0,0 +1,78 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post from '../../models/post'; +import getFriends from '../../common/get-friends'; +import { pack } from '../../models/post'; + +/** + * Get mentions of myself + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'following' parameter + const [following = false, followingError] = + $(params.following).optional.boolean().$; + if (followingError) return rej('invalid following param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + // Construct query + const query = { + mentions: user._id + } as any; + + const sort = { + _id: -1 + }; + + if (following) { + const followingIds = await getFriends(user._id); + + query.user_id = { + $in: followingIds + }; + } + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const mentions = await Post + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(mentions.map(async mention => + await pack(mention, user) + ))); +}); diff --git a/src/server/api/endpoints/posts/polls/recommendation.ts b/src/server/api/endpoints/posts/polls/recommendation.ts new file mode 100644 index 0000000000..4a3fa3f55e --- /dev/null +++ b/src/server/api/endpoints/posts/polls/recommendation.ts @@ -0,0 +1,59 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Vote from '../../../models/poll-vote'; +import Post, { pack } from '../../../models/post'; + +/** + * Get recommended polls + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get votes + const votes = await Vote.find({ + user_id: user._id + }, { + fields: { + _id: false, + post_id: true + } + }); + + const nin = votes && votes.length != 0 ? votes.map(v => v.post_id) : []; + + const posts = await Post + .find({ + _id: { + $nin: nin + }, + user_id: { + $ne: user._id + }, + poll: { + $exists: true, + $ne: null + } + }, { + limit: limit, + skip: offset, + sort: { + _id: -1 + } + }); + + // Serialize + res(await Promise.all(posts.map(async post => + await pack(post, user, { detail: true })))); +}); diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts new file mode 100644 index 0000000000..16ce76a6fa --- /dev/null +++ b/src/server/api/endpoints/posts/polls/vote.ts @@ -0,0 +1,115 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Vote from '../../../models/poll-vote'; +import Post from '../../../models/post'; +import Watching from '../../../models/post-watching'; +import notify from '../../../common/notify'; +import watch from '../../../common/watch-post'; +import { publishPostStream } from '../../../event'; + +/** + * Vote poll of a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Get votee + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + if (post.poll == null) { + return rej('poll not found'); + } + + // Get 'choice' parameter + const [choice, choiceError] = + $(params.choice).number() + .pipe(c => post.poll.choices.some(x => x.id == c)) + .$; + if (choiceError) return rej('invalid choice param'); + + // if already voted + const exist = await Vote.findOne({ + post_id: post._id, + user_id: user._id + }); + + if (exist !== null) { + return rej('already voted'); + } + + // Create vote + await Vote.insert({ + created_at: new Date(), + post_id: post._id, + user_id: user._id, + choice: choice + }); + + // Send response + res(); + + const inc = {}; + inc[`poll.choices.${findWithAttr(post.poll.choices, 'id', choice)}.votes`] = 1; + + // Increment votes count + await Post.update({ _id: post._id }, { + $inc: inc + }); + + publishPostStream(post._id, 'poll_voted'); + + // Notify + notify(post.user_id, user._id, 'poll_vote', { + post_id: post._id, + choice: choice + }); + + // Fetch watchers + Watching + .find({ + post_id: post._id, + user_id: { $ne: user._id }, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }, { + fields: { + user_id: true + } + }) + .then(watchers => { + watchers.forEach(watcher => { + notify(watcher.user_id, user._id, 'poll_vote', { + post_id: post._id, + choice: choice + }); + }); + }); + + // この投稿をWatchする + if (user.account.settings.auto_watch !== false) { + watch(user._id, post); + } +}); + +function findWithAttr(array, attr, value) { + for (let i = 0; i < array.length; i += 1) { + if (array[i][attr] === value) { + return i; + } + } + return -1; +} diff --git a/src/server/api/endpoints/posts/reactions.ts b/src/server/api/endpoints/posts/reactions.ts new file mode 100644 index 0000000000..feb140ab41 --- /dev/null +++ b/src/server/api/endpoints/posts/reactions.ts @@ -0,0 +1,57 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post from '../../models/post'; +import Reaction, { pack } from '../../models/post-reaction'; + +/** + * Show reactions of a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'sort' parameter + const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$; + if (sortError) return rej('invalid sort param'); + + // Lookup post + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + // Issue query + const reactions = await Reaction + .find({ + post_id: post._id, + deleted_at: { $exists: false } + }, { + limit: limit, + skip: offset, + sort: { + _id: sort == 'asc' ? 1 : -1 + } + }); + + // Serialize + res(await Promise.all(reactions.map(async reaction => + await pack(reaction, user)))); +}); diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts new file mode 100644 index 0000000000..f77afed40c --- /dev/null +++ b/src/server/api/endpoints/posts/reactions/create.ts @@ -0,0 +1,122 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Reaction from '../../../models/post-reaction'; +import Post, { pack as packPost } from '../../../models/post'; +import { pack as packUser } from '../../../models/user'; +import Watching from '../../../models/post-watching'; +import notify from '../../../common/notify'; +import watch from '../../../common/watch-post'; +import { publishPostStream, pushSw } from '../../../event'; + +/** + * React to a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Get 'reaction' parameter + const [reaction, reactionErr] = $(params.reaction).string().or([ + 'like', + 'love', + 'laugh', + 'hmm', + 'surprise', + 'congrats', + 'angry', + 'confused', + 'pudding' + ]).$; + if (reactionErr) return rej('invalid reaction param'); + + // Fetch reactee + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + // Myself + if (post.user_id.equals(user._id)) { + return rej('cannot react to my post'); + } + + // if already reacted + const exist = await Reaction.findOne({ + post_id: post._id, + user_id: user._id, + deleted_at: { $exists: false } + }); + + if (exist !== null) { + return rej('already reacted'); + } + + // Create reaction + await Reaction.insert({ + created_at: new Date(), + post_id: post._id, + user_id: user._id, + reaction: reaction + }); + + // Send response + res(); + + const inc = {}; + inc[`reaction_counts.${reaction}`] = 1; + + // Increment reactions count + await Post.update({ _id: post._id }, { + $inc: inc + }); + + publishPostStream(post._id, 'reacted'); + + // Notify + notify(post.user_id, user._id, 'reaction', { + post_id: post._id, + reaction: reaction + }); + + pushSw(post.user_id, 'reaction', { + user: await packUser(user, post.user_id), + post: await packPost(post, post.user_id), + reaction: reaction + }); + + // Fetch watchers + Watching + .find({ + post_id: post._id, + user_id: { $ne: user._id }, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }, { + fields: { + user_id: true + } + }) + .then(watchers => { + watchers.forEach(watcher => { + notify(watcher.user_id, user._id, 'reaction', { + post_id: post._id, + reaction: reaction + }); + }); + }); + + // この投稿をWatchする + if (user.account.settings.auto_watch !== false) { + watch(user._id, post); + } +}); diff --git a/src/server/api/endpoints/posts/reactions/delete.ts b/src/server/api/endpoints/posts/reactions/delete.ts new file mode 100644 index 0000000000..922c57ab18 --- /dev/null +++ b/src/server/api/endpoints/posts/reactions/delete.ts @@ -0,0 +1,60 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Reaction from '../../../models/post-reaction'; +import Post from '../../../models/post'; +// import event from '../../../event'; + +/** + * Unreact to a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Fetch unreactee + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + // if already unreacted + const exist = await Reaction.findOne({ + post_id: post._id, + user_id: user._id, + deleted_at: { $exists: false } + }); + + if (exist === null) { + return rej('never reacted'); + } + + // Delete reaction + await Reaction.update({ + _id: exist._id + }, { + $set: { + deleted_at: new Date() + } + }); + + // Send response + res(); + + const dec = {}; + dec[`reaction_counts.${exist.reaction}`] = -1; + + // Decrement reactions count + Post.update({ _id: post._id }, { + $inc: dec + }); +}); diff --git a/src/server/api/endpoints/posts/replies.ts b/src/server/api/endpoints/posts/replies.ts new file mode 100644 index 0000000000..613c4fa24c --- /dev/null +++ b/src/server/api/endpoints/posts/replies.ts @@ -0,0 +1,53 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post, { pack } from '../../models/post'; + +/** + * Show a replies of a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'sort' parameter + const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$; + if (sortError) return rej('invalid sort param'); + + // Lookup post + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + // Issue query + const replies = await Post + .find({ reply_id: post._id }, { + limit: limit, + skip: offset, + sort: { + _id: sort == 'asc' ? 1 : -1 + } + }); + + // Serialize + res(await Promise.all(replies.map(async post => + await pack(post, user)))); +}); diff --git a/src/server/api/endpoints/posts/reposts.ts b/src/server/api/endpoints/posts/reposts.ts new file mode 100644 index 0000000000..89ab0e3d55 --- /dev/null +++ b/src/server/api/endpoints/posts/reposts.ts @@ -0,0 +1,73 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post, { pack } from '../../models/post'; + +/** + * Show a reposts of a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Check if both of since_id and until_id is specified + if (sinceId && untilId) { + return rej('cannot set since_id and until_id'); + } + + // Lookup post + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + // Construct query + const sort = { + _id: -1 + }; + const query = { + repost_id: post._id + } as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const reposts = await Post + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(reposts.map(async post => + await pack(post, user)))); +}); diff --git a/src/server/api/endpoints/posts/search.ts b/src/server/api/endpoints/posts/search.ts new file mode 100644 index 0000000000..a36d1178a5 --- /dev/null +++ b/src/server/api/endpoints/posts/search.ts @@ -0,0 +1,364 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +const escapeRegexp = require('escape-regexp'); +import Post from '../../models/post'; +import User from '../../models/user'; +import Mute from '../../models/mute'; +import getFriends from '../../common/get-friends'; +import { pack } from '../../models/post'; + +/** + * Search a post + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'text' parameter + const [text, textError] = $(params.text).optional.string().$; + if (textError) return rej('invalid text param'); + + // Get 'include_user_ids' parameter + const [includeUserIds = [], includeUserIdsErr] = $(params.include_user_ids).optional.array('id').$; + if (includeUserIdsErr) return rej('invalid include_user_ids param'); + + // Get 'exclude_user_ids' parameter + const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_user_ids).optional.array('id').$; + if (excludeUserIdsErr) return rej('invalid exclude_user_ids param'); + + // Get 'include_user_usernames' parameter + const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.include_user_usernames).optional.array('string').$; + if (includeUserUsernamesErr) return rej('invalid include_user_usernames param'); + + // Get 'exclude_user_usernames' parameter + const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.exclude_user_usernames).optional.array('string').$; + if (excludeUserUsernamesErr) return rej('invalid exclude_user_usernames param'); + + // Get 'following' parameter + const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$; + if (followingErr) return rej('invalid following param'); + + // Get 'mute' parameter + const [mute = 'mute_all', muteErr] = $(params.mute).optional.string().$; + if (muteErr) return rej('invalid mute param'); + + // Get 'reply' parameter + const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$; + if (replyErr) return rej('invalid reply param'); + + // Get 'repost' parameter + const [repost = null, repostErr] = $(params.repost).optional.nullable.boolean().$; + if (repostErr) return rej('invalid repost param'); + + // Get 'media' parameter + const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$; + if (mediaErr) return rej('invalid media param'); + + // Get 'poll' parameter + const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$; + if (pollErr) return rej('invalid poll param'); + + // Get 'since_date' parameter + const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$; + if (sinceDateErr) throw 'invalid since_date param'; + + // Get 'until_date' parameter + const [untilDate, untilDateErr] = $(params.until_date).optional.number().$; + if (untilDateErr) throw 'invalid until_date param'; + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$; + if (limitErr) return rej('invalid limit param'); + + let includeUsers = includeUserIds; + if (includeUserUsernames != null) { + const ids = (await Promise.all(includeUserUsernames.map(async (username) => { + const _user = await User.findOne({ + username_lower: username.toLowerCase() + }); + return _user ? _user._id : null; + }))).filter(id => id != null); + includeUsers = includeUsers.concat(ids); + } + + let excludeUsers = excludeUserIds; + if (excludeUserUsernames != null) { + const ids = (await Promise.all(excludeUserUsernames.map(async (username) => { + const _user = await User.findOne({ + username_lower: username.toLowerCase() + }); + return _user ? _user._id : null; + }))).filter(id => id != null); + excludeUsers = excludeUsers.concat(ids); + } + + search(res, rej, me, text, includeUsers, excludeUsers, following, + mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit); +}); + +async function search( + res, rej, me, text, includeUserIds, excludeUserIds, following, + mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) { + + let q: any = { + $and: [] + }; + + const push = x => q.$and.push(x); + + if (text) { + // 完全一致検索 + if (/"""(.+?)"""/.test(text)) { + const x = text.match(/"""(.+?)"""/)[1]; + push({ + text: x + }); + } else { + const tags = text.split(' ').filter(x => x[0] == '#'); + if (tags) { + push({ + $and: tags.map(x => ({ + tags: x + })) + }); + } + + push({ + $and: text.split(' ').map(x => ({ + // キーワードが-で始まる場合そのキーワードを除外する + text: x[0] == '-' ? { + $not: new RegExp(escapeRegexp(x.substr(1))) + } : new RegExp(escapeRegexp(x)) + })) + }); + } + } + + if (includeUserIds && includeUserIds.length != 0) { + push({ + user_id: { + $in: includeUserIds + } + }); + } else if (excludeUserIds && excludeUserIds.length != 0) { + push({ + user_id: { + $nin: excludeUserIds + } + }); + } + + if (following != null && me != null) { + const ids = await getFriends(me._id, false); + push({ + user_id: following ? { + $in: ids + } : { + $nin: ids.concat(me._id) + } + }); + } + + if (me != null) { + const mutes = await Mute.find({ + muter_id: me._id, + deleted_at: { $exists: false } + }); + const mutedUserIds = mutes.map(m => m.mutee_id); + + switch (mute) { + case 'mute_all': + push({ + user_id: { + $nin: mutedUserIds + }, + '_reply.user_id': { + $nin: mutedUserIds + }, + '_repost.user_id': { + $nin: mutedUserIds + } + }); + break; + case 'mute_related': + push({ + '_reply.user_id': { + $nin: mutedUserIds + }, + '_repost.user_id': { + $nin: mutedUserIds + } + }); + break; + case 'mute_direct': + push({ + user_id: { + $nin: mutedUserIds + } + }); + break; + case 'direct_only': + push({ + user_id: { + $in: mutedUserIds + } + }); + break; + case 'related_only': + push({ + $or: [{ + '_reply.user_id': { + $in: mutedUserIds + } + }, { + '_repost.user_id': { + $in: mutedUserIds + } + }] + }); + break; + case 'all_only': + push({ + $or: [{ + user_id: { + $in: mutedUserIds + } + }, { + '_reply.user_id': { + $in: mutedUserIds + } + }, { + '_repost.user_id': { + $in: mutedUserIds + } + }] + }); + break; + } + } + + if (reply != null) { + if (reply) { + push({ + reply_id: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + reply_id: { + $exists: false + } + }, { + reply_id: null + }] + }); + } + } + + if (repost != null) { + if (repost) { + push({ + repost_id: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + repost_id: { + $exists: false + } + }, { + repost_id: null + }] + }); + } + } + + if (media != null) { + if (media) { + push({ + media_ids: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + media_ids: { + $exists: false + } + }, { + media_ids: null + }] + }); + } + } + + if (poll != null) { + if (poll) { + push({ + poll: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + poll: { + $exists: false + } + }, { + poll: null + }] + }); + } + } + + if (sinceDate) { + push({ + created_at: { + $gt: new Date(sinceDate) + } + }); + } + + if (untilDate) { + push({ + created_at: { + $lt: new Date(untilDate) + } + }); + } + + if (q.$and.length == 0) { + q = {}; + } + + // Search posts + const posts = await Post + .find(q, { + sort: { + _id: -1 + }, + limit: max, + skip: offset + }); + + // Serialize + res(await Promise.all(posts.map(async post => + await pack(post, me)))); +} diff --git a/src/server/api/endpoints/posts/show.ts b/src/server/api/endpoints/posts/show.ts new file mode 100644 index 0000000000..3839490597 --- /dev/null +++ b/src/server/api/endpoints/posts/show.ts @@ -0,0 +1,32 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post, { pack } from '../../models/post'; + +/** + * Show a post + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'post_id' parameter + const [postId, postIdErr] = $(params.post_id).id().$; + if (postIdErr) return rej('invalid post_id param'); + + // Get post + const post = await Post.findOne({ + _id: postId + }); + + if (post === null) { + return rej('post not found'); + } + + // Serialize + res(await pack(post, user, { + detail: true + })); +}); diff --git a/src/server/api/endpoints/posts/timeline.ts b/src/server/api/endpoints/posts/timeline.ts new file mode 100644 index 0000000000..c41cfdb8bd --- /dev/null +++ b/src/server/api/endpoints/posts/timeline.ts @@ -0,0 +1,132 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import rap from '@prezzemolo/rap'; +import Post from '../../models/post'; +import Mute from '../../models/mute'; +import ChannelWatching from '../../models/channel-watching'; +import getFriends from '../../common/get-friends'; +import { pack } from '../../models/post'; + +/** + * Get timeline of myself + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise} + */ +module.exports = async (params, user, app) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) throw 'invalid limit param'; + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) throw 'invalid since_id param'; + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) throw 'invalid until_id param'; + + // Get 'since_date' parameter + const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$; + if (sinceDateErr) throw 'invalid since_date param'; + + // Get 'until_date' parameter + const [untilDate, untilDateErr] = $(params.until_date).optional.number().$; + if (untilDateErr) throw 'invalid until_date param'; + + // Check if only one of since_id, until_id, since_date, until_date specified + if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + throw 'only one of since_id, until_id, since_date, until_date can be specified'; + } + + const { followingIds, watchingChannelIds, mutedUserIds } = await rap({ + // ID list of the user itself and other users who the user follows + followingIds: getFriends(user._id), + + // Watchしているチャンネルを取得 + watchingChannelIds: ChannelWatching.find({ + user_id: user._id, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }).then(watches => watches.map(w => w.channel_id)), + + // ミュートしているユーザーを取得 + mutedUserIds: Mute.find({ + muter_id: user._id, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }).then(ms => ms.map(m => m.mutee_id)) + }); + + //#region Construct query + const sort = { + _id: -1 + }; + + const query = { + $or: [{ + // フォローしている人のタイムラインへの投稿 + user_id: { + $in: followingIds + }, + // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る + $or: [{ + channel_id: { + $exists: false + } + }, { + channel_id: null + }] + }, { + // Watchしているチャンネルへの投稿 + channel_id: { + $in: watchingChannelIds + } + }], + // mute + user_id: { + $nin: mutedUserIds + }, + '_reply.user_id': { + $nin: mutedUserIds + }, + '_repost.user_id': { + $nin: mutedUserIds + }, + } as any; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } else if (sinceDate) { + sort._id = 1; + query.created_at = { + $gt: new Date(sinceDate) + }; + } else if (untilDate) { + query.created_at = { + $lt: new Date(untilDate) + }; + } + //#endregion + + // Issue query + const timeline = await Post + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + return await Promise.all(timeline.map(post => pack(post, user))); +}; diff --git a/src/server/api/endpoints/posts/trend.ts b/src/server/api/endpoints/posts/trend.ts new file mode 100644 index 0000000000..caded92bf5 --- /dev/null +++ b/src/server/api/endpoints/posts/trend.ts @@ -0,0 +1,79 @@ +/** + * Module dependencies + */ +const ms = require('ms'); +import $ from 'cafy'; +import Post, { pack } from '../../models/post'; + +/** + * Get trend posts + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'reply' parameter + const [reply, replyErr] = $(params.reply).optional.boolean().$; + if (replyErr) return rej('invalid reply param'); + + // Get 'repost' parameter + const [repost, repostErr] = $(params.repost).optional.boolean().$; + if (repostErr) return rej('invalid repost param'); + + // Get 'media' parameter + const [media, mediaErr] = $(params.media).optional.boolean().$; + if (mediaErr) return rej('invalid media param'); + + // Get 'poll' parameter + const [poll, pollErr] = $(params.poll).optional.boolean().$; + if (pollErr) return rej('invalid poll param'); + + const query = { + created_at: { + $gte: new Date(Date.now() - ms('1days')) + }, + repost_count: { + $gt: 0 + } + } as any; + + if (reply != undefined) { + query.reply_id = reply ? { $exists: true, $ne: null } : null; + } + + if (repost != undefined) { + query.repost_id = repost ? { $exists: true, $ne: null } : null; + } + + if (media != undefined) { + query.media_ids = media ? { $exists: true, $ne: null } : null; + } + + if (poll != undefined) { + query.poll = poll ? { $exists: true, $ne: null } : null; + } + + // Issue query + const posts = await Post + .find(query, { + limit: limit, + skip: offset, + sort: { + repost_count: -1, + _id: -1 + } + }); + + // Serialize + res(await Promise.all(posts.map(async post => + await pack(post, user, { detail: true })))); +}); diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts new file mode 100644 index 0000000000..a6084cd17a --- /dev/null +++ b/src/server/api/endpoints/stats.ts @@ -0,0 +1,48 @@ +/** + * Module dependencies + */ +import Post from '../models/post'; +import User from '../models/user'; + +/** + * @swagger + * /stats: + * post: + * summary: Show the misskey's statistics + * responses: + * 200: + * description: Success + * schema: + * type: object + * properties: + * posts_count: + * description: count of all posts of misskey + * type: number + * users_count: + * description: count of all users of misskey + * type: number + * + * default: + * description: Failed + * schema: + * $ref: "#/definitions/Error" + */ + +/** + * Show the misskey's statistics + * + * @param {any} params + * @return {Promise} + */ +module.exports = params => new Promise(async (res, rej) => { + const postsCount = await Post + .count(); + + const usersCount = await User + .count(); + + res({ + posts_count: postsCount, + users_count: usersCount + }); +}); diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts new file mode 100644 index 0000000000..99406138db --- /dev/null +++ b/src/server/api/endpoints/sw/register.ts @@ -0,0 +1,50 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Subscription from '../../models/sw-subscription'; + +/** + * subscribe service worker + * + * @param {any} params + * @param {any} user + * @param {any} _ + * @param {boolean} isSecure + * @return {Promise} + */ +module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => { + // Get 'endpoint' parameter + const [endpoint, endpointErr] = $(params.endpoint).string().$; + if (endpointErr) return rej('invalid endpoint param'); + + // Get 'auth' parameter + const [auth, authErr] = $(params.auth).string().$; + if (authErr) return rej('invalid auth param'); + + // Get 'publickey' parameter + const [publickey, publickeyErr] = $(params.publickey).string().$; + if (publickeyErr) return rej('invalid publickey param'); + + // if already subscribed + const exist = await Subscription.findOne({ + user_id: user._id, + endpoint: endpoint, + auth: auth, + publickey: publickey, + deleted_at: { $exists: false } + }); + + if (exist !== null) { + return res(); + } + + await Subscription.insert({ + user_id: user._id, + endpoint: endpoint, + auth: auth, + publickey: publickey + }); + + res(); +}); diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts new file mode 100644 index 0000000000..aac7fadf5a --- /dev/null +++ b/src/server/api/endpoints/username/available.ts @@ -0,0 +1,32 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../models/user'; +import { validateUsername } from '../../models/user'; + +/** + * Check available username + * + * @param {any} params + * @return {Promise} + */ +module.exports = async (params) => new Promise(async (res, rej) => { + // Get 'username' parameter + const [username, usernameError] = $(params.username).string().pipe(validateUsername).$; + if (usernameError) return rej('invalid username param'); + + // Get exist + const exist = await User + .count({ + host: null, + username_lower: username.toLowerCase() + }, { + limit: 1 + }); + + // Reply + res({ + available: exist === 0 + }); +}); diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts new file mode 100644 index 0000000000..4acc13c281 --- /dev/null +++ b/src/server/api/endpoints/users.ts @@ -0,0 +1,56 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { pack } from '../models/user'; + +/** + * Lists all users + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'sort' parameter + const [sort, sortError] = $(params.sort).optional.string().or('+follower|-follower').$; + if (sortError) return rej('invalid sort param'); + + // Construct query + let _sort; + if (sort) { + if (sort == '+follower') { + _sort = { + followers_count: -1 + }; + } else if (sort == '-follower') { + _sort = { + followers_count: 1 + }; + } + } else { + _sort = { + _id: -1 + }; + } + + // Issue query + const users = await User + .find({}, { + limit: limit, + sort: _sort, + skip: offset + }); + + // Serialize + res(await Promise.all(users.map(async user => + await pack(user, me)))); +}); diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts new file mode 100644 index 0000000000..b0fb83c683 --- /dev/null +++ b/src/server/api/endpoints/users/followers.ts @@ -0,0 +1,92 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../models/user'; +import Following from '../../models/following'; +import { pack } from '../../models/user'; +import getFriends from '../../common/get-friends'; + +/** + * Get followers of a user + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Get 'iknow' parameter + const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$; + if (iknowErr) return rej('invalid iknow param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'cursor' parameter + const [cursor = null, cursorErr] = $(params.cursor).optional.id().$; + if (cursorErr) return rej('invalid cursor param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + // Construct query + const query = { + followee_id: user._id, + deleted_at: { $exists: false } + } as any; + + // ログインしていてかつ iknow フラグがあるとき + if (me && iknow) { + // Get my friends + const myFriends = await getFriends(me._id); + + query.follower_id = { + $in: myFriends + }; + } + + // カーソルが指定されている場合 + if (cursor) { + query._id = { + $lt: cursor + }; + } + + // Get followers + const following = await Following + .find(query, { + limit: limit + 1, + sort: { _id: -1 } + }); + + // 「次のページ」があるかどうか + const inStock = following.length === limit + 1; + if (inStock) { + following.pop(); + } + + // Serialize + const users = await Promise.all(following.map(async f => + await pack(f.follower_id, me, { detail: true }))); + + // Response + res({ + users: users, + next: inStock ? following[following.length - 1]._id : null, + }); +}); diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts new file mode 100644 index 0000000000..8e88431e92 --- /dev/null +++ b/src/server/api/endpoints/users/following.ts @@ -0,0 +1,92 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../models/user'; +import Following from '../../models/following'; +import { pack } from '../../models/user'; +import getFriends from '../../common/get-friends'; + +/** + * Get following users of a user + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Get 'iknow' parameter + const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$; + if (iknowErr) return rej('invalid iknow param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'cursor' parameter + const [cursor = null, cursorErr] = $(params.cursor).optional.id().$; + if (cursorErr) return rej('invalid cursor param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + // Construct query + const query = { + follower_id: user._id, + deleted_at: { $exists: false } + } as any; + + // ログインしていてかつ iknow フラグがあるとき + if (me && iknow) { + // Get my friends + const myFriends = await getFriends(me._id); + + query.followee_id = { + $in: myFriends + }; + } + + // カーソルが指定されている場合 + if (cursor) { + query._id = { + $lt: cursor + }; + } + + // Get followers + const following = await Following + .find(query, { + limit: limit + 1, + sort: { _id: -1 } + }); + + // 「次のページ」があるかどうか + const inStock = following.length === limit + 1; + if (inStock) { + following.pop(); + } + + // Serialize + const users = await Promise.all(following.map(async f => + await pack(f.followee_id, me, { detail: true }))); + + // Response + res({ + users: users, + next: inStock ? following[following.length - 1]._id : null, + }); +}); diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts new file mode 100644 index 0000000000..87f4f77a5b --- /dev/null +++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts @@ -0,0 +1,99 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Post from '../../models/post'; +import User, { pack } from '../../models/user'; + +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + // Fetch recent posts + const recentPosts = await Post.find({ + user_id: user._id, + reply_id: { + $exists: true, + $ne: null + } + }, { + sort: { + _id: -1 + }, + limit: 1000, + fields: { + _id: false, + reply_id: true + } + }); + + // 投稿が少なかったら中断 + if (recentPosts.length === 0) { + return res([]); + } + + const replyTargetPosts = await Post.find({ + _id: { + $in: recentPosts.map(p => p.reply_id) + }, + user_id: { + $ne: user._id + } + }, { + fields: { + _id: false, + user_id: true + } + }); + + const repliedUsers = {}; + + // Extract replies from recent posts + replyTargetPosts.forEach(post => { + const userId = post.user_id.toString(); + if (repliedUsers[userId]) { + repliedUsers[userId]++; + } else { + repliedUsers[userId] = 1; + } + }); + + // Calc peak + let peak = 0; + Object.keys(repliedUsers).forEach(user => { + if (repliedUsers[user] > peak) peak = repliedUsers[user]; + }); + + // Sort replies by frequency + const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); + + // Extract top replied users + const topRepliedUsers = repliedUsersSorted.slice(0, limit); + + // Make replies object (includes weights) + const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ + user: await pack(user, me, { detail: true }), + weight: repliedUsers[user] / peak + }))); + + // Response + res(repliesObj); +}); diff --git a/src/server/api/endpoints/users/posts.ts b/src/server/api/endpoints/users/posts.ts new file mode 100644 index 0000000000..3c84bf0d80 --- /dev/null +++ b/src/server/api/endpoints/users/posts.ts @@ -0,0 +1,137 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import getHostLower from '../../common/get-host-lower'; +import Post, { pack } from '../../models/post'; +import User from '../../models/user'; + +/** + * Get posts of a user + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).optional.id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Get 'username' parameter + const [username, usernameErr] = $(params.username).optional.string().$; + if (usernameErr) return rej('invalid username param'); + + if (userId === undefined && username === undefined) { + return rej('user_id or pair of username and host is required'); + } + + // Get 'host' parameter + const [host, hostErr] = $(params.host).optional.string().$; + if (hostErr) return rej('invalid host param'); + + if (userId === undefined && host === undefined) { + return rej('user_id or pair of username and host is required'); + } + + // Get 'include_replies' parameter + const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$; + if (includeRepliesErr) return rej('invalid include_replies param'); + + // Get 'with_media' parameter + const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$; + if (withMediaErr) return rej('invalid with_media param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'until_id' parameter + const [untilId, untilIdErr] = $(params.until_id).optional.id().$; + if (untilIdErr) return rej('invalid until_id param'); + + // Get 'since_date' parameter + const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$; + if (sinceDateErr) throw 'invalid since_date param'; + + // Get 'until_date' parameter + const [untilDate, untilDateErr] = $(params.until_date).optional.number().$; + if (untilDateErr) throw 'invalid until_date param'; + + // Check if only one of since_id, until_id, since_date, until_date specified + if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + throw 'only one of since_id, until_id, since_date, until_date can be specified'; + } + + const q = userId !== undefined + ? { _id: userId } + : { username_lower: username.toLowerCase(), host_lower: getHostLower(host) } ; + + // Lookup user + const user = await User.findOne(q, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + //#region Construct query + const sort = { + _id: -1 + }; + + const query = { + user_id: user._id + } as any; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } else if (sinceDate) { + sort._id = 1; + query.created_at = { + $gt: new Date(sinceDate) + }; + } else if (untilDate) { + query.created_at = { + $lt: new Date(untilDate) + }; + } + + if (!includeReplies) { + query.reply_id = null; + } + + if (withMedia) { + query.media_ids = { + $exists: true, + $ne: null + }; + } + //#endregion + + // Issue query + const posts = await Post + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(posts.map(async (post) => + await pack(post, me) + ))); +}); diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts new file mode 100644 index 0000000000..45d90f422b --- /dev/null +++ b/src/server/api/endpoints/users/recommendation.ts @@ -0,0 +1,53 @@ +/** + * Module dependencies + */ +const ms = require('ms'); +import $ from 'cafy'; +import User, { pack } from '../../models/user'; +import getFriends from '../../common/get-friends'; + +/** + * Get recommended users + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // ID list of the user itself and other users who the user follows + const followingIds = await getFriends(me._id); + + const users = await User + .find({ + _id: { + $nin: followingIds + }, + $or: [ + { + 'account.last_used_at': { + $gte: new Date(Date.now() - ms('7days')) + } + }, { + host: { $not: null } + } + ] + }, { + limit: limit, + skip: offset, + sort: { + followers_count: -1 + } + }); + + // Serialize + res(await Promise.all(users.map(async user => + await pack(user, me, { detail: true })))); +}); diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts new file mode 100644 index 0000000000..3c81576440 --- /dev/null +++ b/src/server/api/endpoints/users/search.ts @@ -0,0 +1,98 @@ +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import $ from 'cafy'; +import User, { pack } from '../../models/user'; +import config from '../../../../conf'; +const escapeRegexp = require('escape-regexp'); + +/** + * Search a user + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'query' parameter + const [query, queryError] = $(params.query).string().pipe(x => x != '').$; + if (queryError) return rej('invalid query param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'max' parameter + const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$; + if (maxErr) return rej('invalid max param'); + + // If Elasticsearch is available, search by $ + // If not, search by MongoDB + (config.elasticsearch.enable ? byElasticsearch : byNative) + (res, rej, me, query, offset, max); +}); + +// Search by MongoDB +async function byNative(res, rej, me, query, offset, max) { + const escapedQuery = escapeRegexp(query); + + // Search users + const users = await User + .find({ + $or: [{ + username_lower: new RegExp(escapedQuery.replace('@', '').toLowerCase()) + }, { + name: new RegExp(escapedQuery) + }] + }, { + limit: max + }); + + // Serialize + res(await Promise.all(users.map(async user => + await pack(user, me, { detail: true })))); +} + +// Search by Elasticsearch +async function byElasticsearch(res, rej, me, query, offset, max) { + const es = require('../../db/elasticsearch'); + + es.search({ + index: 'misskey', + type: 'user', + body: { + size: max, + from: offset, + query: { + simple_query_string: { + fields: ['username', 'name', 'bio'], + query: query, + default_operator: 'and' + } + } + } + }, async (error, response) => { + if (error) { + console.error(error); + return res(500); + } + + if (response.hits.total === 0) { + return res([]); + } + + const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id)); + + const users = await User + .find({ + _id: { + $in: hits + } + }); + + // Serialize + res(await Promise.all(users.map(async user => + await pack(user, me, { detail: true })))); + }); +} diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts new file mode 100644 index 0000000000..9c5e1905aa --- /dev/null +++ b/src/server/api/endpoints/users/search_by_username.ts @@ -0,0 +1,38 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { pack } from '../../models/user'; + +/** + * Search a user by username + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'query' parameter + const [query, queryError] = $(params.query).string().$; + if (queryError) return rej('invalid query param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + const users = await User + .find({ + username_lower: new RegExp(query.toLowerCase()) + }, { + limit: limit, + skip: offset + }); + + // Serialize + res(await Promise.all(users.map(async user => + await pack(user, me, { detail: true })))); +}); diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts new file mode 100644 index 0000000000..78df23f339 --- /dev/null +++ b/src/server/api/endpoints/users/show.ts @@ -0,0 +1,209 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import { JSDOM } from 'jsdom'; +import { toUnicode, toASCII } from 'punycode'; +import uploadFromUrl from '../../common/drive/upload_from_url'; +import User, { pack, validateUsername, isValidName, isValidDescription } from '../../models/user'; +const request = require('request-promise-native'); +const WebFinger = require('webfinger.js'); + +const webFinger = new WebFinger({}); + +async function getCollectionCount(url) { + if (!url) { + return null; + } + + try { + const collection = await request({ url, json: true }); + return collection ? collection.totalItems : null; + } catch (exception) { + return null; + } +} + +function findUser(q) { + return User.findOne(q, { + fields: { + data: false + } + }); +} + +function webFingerAndVerify(query, verifier) { + return new Promise((res, rej) => webFinger.lookup(query, (error, result) => { + if (error) { + return rej(error); + } + + if (result.object.subject.toLowerCase().replace(/^acct:/, '') !== verifier) { + return rej('WebFinger verfification failed'); + } + + res(result.object); + })); +} + +/** + * Show a user + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + let user; + + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).optional.id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Get 'username' parameter + const [username, usernameErr] = $(params.username).optional.string().$; + if (usernameErr) return rej('invalid username param'); + + // Get 'host' parameter + const [host, hostErr] = $(params.host).optional.string().$; + if (hostErr) return rej('invalid username param'); + + if (userId === undefined && typeof username !== 'string') { + return rej('user_id or pair of username and host is required'); + } + + // Lookup user + if (typeof host === 'string') { + const username_lower = username.toLowerCase(); + const host_lower_ascii = toASCII(host).toLowerCase(); + const host_lower = toUnicode(host_lower_ascii); + + user = await findUser({ username_lower, host_lower }); + + if (user === null) { + const acct_lower = `${username_lower}@${host_lower_ascii}`; + let activityStreams; + let finger; + let followers_count; + let following_count; + let likes_count; + let posts_count; + + if (!validateUsername(username)) { + return rej('username validation failed'); + } + + try { + finger = await webFingerAndVerify(acct_lower, acct_lower); + } catch (exception) { + return rej('WebFinger lookup failed'); + } + + const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); + if (!self) { + return rej('WebFinger has no reference to self representation'); + } + + try { + activityStreams = await request({ + url: self.href, + headers: { + Accept: 'application/activity+json, application/ld+json' + }, + json: true + }); + } catch (exception) { + return rej('failed to retrieve ActivityStreams representation'); + } + + if (!(activityStreams && + (Array.isArray(activityStreams['@context']) ? + activityStreams['@context'].includes('https://www.w3.org/ns/activitystreams') : + activityStreams['@context'] === 'https://www.w3.org/ns/activitystreams') && + activityStreams.type === 'Person' && + typeof activityStreams.preferredUsername === 'string' && + activityStreams.preferredUsername.toLowerCase() === username_lower && + isValidName(activityStreams.name) && + isValidDescription(activityStreams.summary) + )) { + return rej('failed ActivityStreams validation'); + } + + try { + [followers_count, following_count, likes_count, posts_count] = await Promise.all([ + getCollectionCount(activityStreams.followers), + getCollectionCount(activityStreams.following), + getCollectionCount(activityStreams.liked), + getCollectionCount(activityStreams.outbox), + webFingerAndVerify(activityStreams.id, acct_lower), + ]); + } catch (exception) { + return rej('failed to fetch assets'); + } + + const summaryDOM = JSDOM.fragment(activityStreams.summary); + + // Create user + user = await User.insert({ + avatar_id: null, + banner_id: null, + created_at: new Date(), + description: summaryDOM.textContent, + followers_count, + following_count, + name: activityStreams.name, + posts_count, + likes_count, + liked_count: 0, + drive_capacity: 1073741824, // 1GB + username: username, + username_lower, + host: toUnicode(finger.subject.replace(/^.*?@/, '')), + host_lower, + account: { + uri: activityStreams.id, + }, + }); + + const [icon, image] = await Promise.all([ + activityStreams.icon, + activityStreams.image, + ].map(async image => { + if (!image || image.type !== 'Image') { + return { _id: null }; + } + + try { + return await uploadFromUrl(image.url, user); + } catch (exception) { + return { _id: null }; + } + })); + + User.update({ _id: user._id }, { + $set: { + avatar_id: icon._id, + banner_id: image._id, + }, + }); + + user.avatar_id = icon._id; + user.banner_id = icon._id; + } + } else { + const q = userId !== undefined + ? { _id: userId } + : { username_lower: username.toLowerCase(), host: null }; + + user = await findUser(q); + + if (user === null) { + return rej('user not found'); + } + } + + // Send response + res(await pack(user, me, { + detail: true + })); +}); diff --git a/src/server/api/event.ts b/src/server/api/event.ts new file mode 100644 index 0000000000..98bf161137 --- /dev/null +++ b/src/server/api/event.ts @@ -0,0 +1,80 @@ +import * as mongo from 'mongodb'; +import * as redis from 'redis'; +import swPush from './common/push-sw'; +import config from '../../conf'; + +type ID = string | mongo.ObjectID; + +class MisskeyEvent { + private redisClient: redis.RedisClient; + + constructor() { + // Connect to Redis + this.redisClient = redis.createClient( + config.redis.port, config.redis.host); + } + + public publishUserStream(userId: ID, type: string, value?: any): void { + this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishSw(userId: ID, type: string, value?: any): void { + swPush(userId, type, value); + } + + public publishDriveStream(userId: ID, type: string, value?: any): void { + this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishPostStream(postId: ID, type: string, value?: any): void { + this.publish(`post-stream:${postId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: any): void { + this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishMessagingIndexStream(userId: ID, type: string, value?: any): void { + this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishOthelloStream(userId: ID, type: string, value?: any): void { + this.publish(`othello-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishOthelloGameStream(gameId: ID, type: string, value?: any): void { + this.publish(`othello-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishChannelStream(channelId: ID, type: string, value?: any): void { + this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value); + } + + private publish(channel: string, type: string, value?: any): void { + const message = value == null ? + { type: type } : + { type: type, body: value }; + + this.redisClient.publish(`misskey:${channel}`, JSON.stringify(message)); + } +} + +const ev = new MisskeyEvent(); + +export default ev.publishUserStream.bind(ev); + +export const pushSw = ev.publishSw.bind(ev); + +export const publishDriveStream = ev.publishDriveStream.bind(ev); + +export const publishPostStream = ev.publishPostStream.bind(ev); + +export const publishMessagingStream = ev.publishMessagingStream.bind(ev); + +export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev); + +export const publishOthelloStream = ev.publishOthelloStream.bind(ev); + +export const publishOthelloGameStream = ev.publishOthelloGameStream.bind(ev); + +export const publishChannelStream = ev.publishChannelStream.bind(ev); diff --git a/src/server/api/limitter.ts b/src/server/api/limitter.ts new file mode 100644 index 0000000000..33337fbb1b --- /dev/null +++ b/src/server/api/limitter.ts @@ -0,0 +1,83 @@ +import * as Limiter from 'ratelimiter'; +import * as debug from 'debug'; +import limiterDB from '../../db/redis'; +import { Endpoint } from './endpoints'; +import { IAuthContext } from './authenticate'; +import getAcct from '../common/user/get-acct'; + +const log = debug('misskey:limitter'); + +export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, reject) => { + const limitation = endpoint.limit; + + const key = limitation.hasOwnProperty('key') + ? limitation.key + : endpoint.name; + + const hasShortTermLimit = + limitation.hasOwnProperty('minInterval'); + + const hasLongTermLimit = + limitation.hasOwnProperty('duration') && + limitation.hasOwnProperty('max'); + + if (hasShortTermLimit) { + min(); + } else if (hasLongTermLimit) { + max(); + } else { + ok(); + } + + // Short-term limit + function min() { + const minIntervalLimiter = new Limiter({ + id: `${ctx.user._id}:${key}:min`, + duration: limitation.minInterval, + max: 1, + db: limiterDB + }); + + minIntervalLimiter.get((err, info) => { + if (err) { + return reject('ERR'); + } + + log(`@${getAcct(ctx.user)} ${endpoint.name} min remaining: ${info.remaining}`); + + if (info.remaining === 0) { + reject('BRIEF_REQUEST_INTERVAL'); + } else { + if (hasLongTermLimit) { + max(); + } else { + ok(); + } + } + }); + } + + // Long term limit + function max() { + const limiter = new Limiter({ + id: `${ctx.user._id}:${key}`, + duration: limitation.duration, + max: limitation.max, + db: limiterDB + }); + + limiter.get((err, info) => { + if (err) { + return reject('ERR'); + } + + log(`@${getAcct(ctx.user)} ${endpoint.name} max remaining: ${info.remaining}`); + + if (info.remaining === 0) { + reject('RATE_LIMIT_EXCEEDED'); + } else { + ok(); + } + }); + } +}); diff --git a/src/server/api/models/access-token.ts b/src/server/api/models/access-token.ts new file mode 100644 index 0000000000..2bf91f3093 --- /dev/null +++ b/src/server/api/models/access-token.ts @@ -0,0 +1,8 @@ +import db from '../../../db/mongodb'; + +const collection = db.get('access_tokens'); + +(collection as any).createIndex('token'); // fuck type definition +(collection as any).createIndex('hash'); // fuck type definition + +export default collection as any; // fuck type definition diff --git a/src/server/api/models/app.ts b/src/server/api/models/app.ts new file mode 100644 index 0000000000..17db82ecac --- /dev/null +++ b/src/server/api/models/app.ts @@ -0,0 +1,97 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import AccessToken from './access-token'; +import db from '../../../db/mongodb'; +import config from '../../../conf'; + +const App = db.get('apps'); +App.createIndex('name_id'); +App.createIndex('name_id_lower'); +App.createIndex('secret'); +export default App; + +export type IApp = { + _id: mongo.ObjectID; + created_at: Date; + user_id: mongo.ObjectID; + secret: string; +}; + +export function isValidNameId(nameId: string): boolean { + return typeof nameId == 'string' && /^[a-zA-Z0-9\-]{3,30}$/.test(nameId); +} + +/** + * Pack an app for API response + * + * @param {any} app + * @param {any} me? + * @param {any} options? + * @return {Promise} + */ +export const pack = ( + app: any, + me?: any, + options?: { + includeSecret?: boolean, + includeProfileImageIds?: boolean + } +) => new Promise(async (resolve, reject) => { + const opts = options || { + includeSecret: false, + includeProfileImageIds: false + }; + + let _app: any; + + // Populate the app if 'app' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(app)) { + _app = await App.findOne({ + _id: app + }); + } else if (typeof app === 'string') { + _app = await App.findOne({ + _id: new mongo.ObjectID(app) + }); + } else { + _app = deepcopy(app); + } + + // Me + if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) { + if (typeof me === 'string') { + me = new mongo.ObjectID(me); + } else { + me = me._id; + } + } + + // Rename _id to id + _app.id = _app._id; + delete _app._id; + + delete _app.name_id_lower; + + // Visible by only owner + if (!opts.includeSecret) { + delete _app.secret; + } + + _app.icon_url = _app.icon != null + ? `${config.drive_url}/${_app.icon}` + : `${config.drive_url}/app-default.jpg`; + + if (me) { + // 既に連携しているか + const exist = await AccessToken.count({ + app_id: _app.id, + user_id: me, + }, { + limit: 1 + }); + + _app.is_authorized = exist === 1; + } + + resolve(_app); +}); diff --git a/src/server/api/models/appdata.ts b/src/server/api/models/appdata.ts new file mode 100644 index 0000000000..dda3c98934 --- /dev/null +++ b/src/server/api/models/appdata.ts @@ -0,0 +1,3 @@ +import db from '../../../db/mongodb'; + +export default db.get('appdata') as any; // fuck type definition diff --git a/src/server/api/models/auth-session.ts b/src/server/api/models/auth-session.ts new file mode 100644 index 0000000000..a79d901df5 --- /dev/null +++ b/src/server/api/models/auth-session.ts @@ -0,0 +1,45 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../../../db/mongodb'; +import { pack as packApp } from './app'; + +const AuthSession = db.get('auth_sessions'); +export default AuthSession; + +export interface IAuthSession { + _id: mongo.ObjectID; +} + +/** + * Pack an auth session for API response + * + * @param {any} session + * @param {any} me? + * @return {Promise} + */ +export const pack = ( + session: any, + me?: any +) => new Promise(async (resolve, reject) => { + let _session: any; + + // TODO: Populate session if it ID + + _session = deepcopy(session); + + // Me + if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) { + if (typeof me === 'string') { + me = new mongo.ObjectID(me); + } else { + me = me._id; + } + } + + delete _session._id; + + // Populate app + _session.app = await packApp(_session.app_id, me); + + resolve(_session); +}); diff --git a/src/server/api/models/channel-watching.ts b/src/server/api/models/channel-watching.ts new file mode 100644 index 0000000000..4c6fae28d3 --- /dev/null +++ b/src/server/api/models/channel-watching.ts @@ -0,0 +1,3 @@ +import db from '../../../db/mongodb'; + +export default db.get('channel_watching') as any; // fuck type definition diff --git a/src/server/api/models/channel.ts b/src/server/api/models/channel.ts new file mode 100644 index 0000000000..97999bd9e2 --- /dev/null +++ b/src/server/api/models/channel.ts @@ -0,0 +1,74 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import { IUser } from './user'; +import Watching from './channel-watching'; +import db from '../../../db/mongodb'; + +const Channel = db.get('channels'); +export default Channel; + +export type IChannel = { + _id: mongo.ObjectID; + created_at: Date; + title: string; + user_id: mongo.ObjectID; + index: number; +}; + +/** + * Pack a channel for API response + * + * @param channel target + * @param me? serializee + * @return response + */ +export const pack = ( + channel: string | mongo.ObjectID | IChannel, + me?: string | mongo.ObjectID | IUser +) => new Promise(async (resolve, reject) => { + + let _channel: any; + + // Populate the channel if 'channel' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(channel)) { + _channel = await Channel.findOne({ + _id: channel + }); + } else if (typeof channel === 'string') { + _channel = await Channel.findOne({ + _id: new mongo.ObjectID(channel) + }); + } else { + _channel = deepcopy(channel); + } + + // Rename _id to id + _channel.id = _channel._id; + delete _channel._id; + + // Remove needless properties + delete _channel.user_id; + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + if (me) { + //#region Watchしているかどうか + const watch = await Watching.findOne({ + user_id: meId, + channel_id: _channel.id, + deleted_at: { $exists: false } + }); + + _channel.is_watching = watch !== null; + //#endregion + } + + resolve(_channel); +}); diff --git a/src/server/api/models/drive-file.ts b/src/server/api/models/drive-file.ts new file mode 100644 index 0000000000..851a79a0e7 --- /dev/null +++ b/src/server/api/models/drive-file.ts @@ -0,0 +1,113 @@ +import * as mongodb from 'mongodb'; +import deepcopy = require('deepcopy'); +import { pack as packFolder } from './drive-folder'; +import config from '../../../conf'; +import monkDb, { nativeDbConn } from '../../../db/mongodb'; + +const DriveFile = monkDb.get('drive_files.files'); + +export default DriveFile; + +const getGridFSBucket = async (): Promise => { + const db = await nativeDbConn(); + const bucket = new mongodb.GridFSBucket(db, { + bucketName: 'drive_files' + }); + return bucket; +}; + +export { getGridFSBucket }; + +export type IDriveFile = { + _id: mongodb.ObjectID; + uploadDate: Date; + md5: string; + filename: string; + contentType: string; + metadata: { + properties: any; + user_id: mongodb.ObjectID; + folder_id: mongodb.ObjectID; + } +}; + +export function validateFileName(name: string): boolean { + return ( + (name.trim().length > 0) && + (name.length <= 200) && + (name.indexOf('\\') === -1) && + (name.indexOf('/') === -1) && + (name.indexOf('..') === -1) + ); +} + +/** + * Pack a drive file for API response + * + * @param {any} file + * @param {any} options? + * @return {Promise} + */ +export const pack = ( + file: any, + options?: { + detail: boolean + } +) => new Promise(async (resolve, reject) => { + const opts = Object.assign({ + detail: false + }, options); + + let _file: any; + + // Populate the file if 'file' is ID + if (mongodb.ObjectID.prototype.isPrototypeOf(file)) { + _file = await DriveFile.findOne({ + _id: file + }); + } else if (typeof file === 'string') { + _file = await DriveFile.findOne({ + _id: new mongodb.ObjectID(file) + }); + } else { + _file = deepcopy(file); + } + + if (!_file) return reject('invalid file arg.'); + + // rendered target + let _target: any = {}; + + _target.id = _file._id; + _target.created_at = _file.uploadDate; + _target.name = _file.filename; + _target.type = _file.contentType; + _target.datasize = _file.length; + _target.md5 = _file.md5; + + _target = Object.assign(_target, _file.metadata); + + _target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`; + + if (_target.properties == null) _target.properties = {}; + + if (opts.detail) { + if (_target.folder_id) { + // Populate folder + _target.folder = await packFolder(_target.folder_id, { + detail: true + }); + } + + /* + if (_target.tags) { + // Populate tags + _target.tags = await _target.tags.map(async (tag: any) => + await serializeDriveTag(tag) + ); + } + */ + } + + resolve(_target); +}); diff --git a/src/server/api/models/drive-folder.ts b/src/server/api/models/drive-folder.ts new file mode 100644 index 0000000000..505556376a --- /dev/null +++ b/src/server/api/models/drive-folder.ts @@ -0,0 +1,77 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../../../db/mongodb'; +import DriveFile from './drive-file'; + +const DriveFolder = db.get('drive_folders'); +export default DriveFolder; + +export type IDriveFolder = { + _id: mongo.ObjectID; + created_at: Date; + name: string; + user_id: mongo.ObjectID; + parent_id: mongo.ObjectID; +}; + +export function isValidFolderName(name: string): boolean { + return ( + (name.trim().length > 0) && + (name.length <= 200) + ); +} + +/** + * Pack a drive folder for API response + * + * @param {any} folder + * @param {any} options? + * @return {Promise} + */ +export const pack = ( + folder: any, + options?: { + detail: boolean + } +) => new Promise(async (resolve, reject) => { + const opts = Object.assign({ + detail: false + }, options); + + let _folder: any; + + // Populate the folder if 'folder' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(folder)) { + _folder = await DriveFolder.findOne({ _id: folder }); + } else if (typeof folder === 'string') { + _folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) }); + } else { + _folder = deepcopy(folder); + } + + // Rename _id to id + _folder.id = _folder._id; + delete _folder._id; + + if (opts.detail) { + const childFoldersCount = await DriveFolder.count({ + parent_id: _folder.id + }); + + const childFilesCount = await DriveFile.count({ + 'metadata.folder_id': _folder.id + }); + + _folder.folders_count = childFoldersCount; + _folder.files_count = childFilesCount; + } + + if (opts.detail && _folder.parent_id) { + // Populate parent folder + _folder.parent = await pack(_folder.parent_id, { + detail: true + }); + } + + resolve(_folder); +}); diff --git a/src/server/api/models/drive-tag.ts b/src/server/api/models/drive-tag.ts new file mode 100644 index 0000000000..d1c68365a3 --- /dev/null +++ b/src/server/api/models/drive-tag.ts @@ -0,0 +1,3 @@ +import db from '../../../db/mongodb'; + +export default db.get('drive_tags') as any; // fuck type definition diff --git a/src/server/api/models/favorite.ts b/src/server/api/models/favorite.ts new file mode 100644 index 0000000000..3142617643 --- /dev/null +++ b/src/server/api/models/favorite.ts @@ -0,0 +1,3 @@ +import db from '../../../db/mongodb'; + +export default db.get('favorites') as any; // fuck type definition diff --git a/src/server/api/models/following.ts b/src/server/api/models/following.ts new file mode 100644 index 0000000000..92d7b6d31b --- /dev/null +++ b/src/server/api/models/following.ts @@ -0,0 +1,3 @@ +import db from '../../../db/mongodb'; + +export default db.get('following') as any; // fuck type definition diff --git a/src/server/api/models/messaging-history.ts b/src/server/api/models/messaging-history.ts new file mode 100644 index 0000000000..ea9f317eee --- /dev/null +++ b/src/server/api/models/messaging-history.ts @@ -0,0 +1,3 @@ +import db from '../../../db/mongodb'; + +export default db.get('messaging_histories') as any; // fuck type definition diff --git a/src/server/api/models/messaging-message.ts b/src/server/api/models/messaging-message.ts new file mode 100644 index 0000000000..be484d635f --- /dev/null +++ b/src/server/api/models/messaging-message.ts @@ -0,0 +1,81 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import { pack as packUser } from './user'; +import { pack as packFile } from './drive-file'; +import db from '../../../db/mongodb'; +import parse from '../common/text'; + +const MessagingMessage = db.get('messaging_messages'); +export default MessagingMessage; + +export interface IMessagingMessage { + _id: mongo.ObjectID; + created_at: Date; + text: string; + user_id: mongo.ObjectID; + recipient_id: mongo.ObjectID; + is_read: boolean; +} + +export function isValidText(text: string): boolean { + return text.length <= 1000 && text.trim() != ''; +} + +/** + * Pack a messaging message for API response + * + * @param {any} message + * @param {any} me? + * @param {any} options? + * @return {Promise} + */ +export const pack = ( + message: any, + me?: any, + options?: { + populateRecipient: boolean + } +) => new Promise(async (resolve, reject) => { + const opts = options || { + populateRecipient: true + }; + + let _message: any; + + // Populate the message if 'message' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(message)) { + _message = await MessagingMessage.findOne({ + _id: message + }); + } else if (typeof message === 'string') { + _message = await MessagingMessage.findOne({ + _id: new mongo.ObjectID(message) + }); + } else { + _message = deepcopy(message); + } + + // Rename _id to id + _message.id = _message._id; + delete _message._id; + + // Parse text + if (_message.text) { + _message.ast = parse(_message.text); + } + + // Populate user + _message.user = await packUser(_message.user_id, me); + + if (_message.file_id) { + // Populate file + _message.file = await packFile(_message.file_id); + } + + if (opts.populateRecipient) { + // Populate recipient + _message.recipient = await packUser(_message.recipient_id, me); + } + + resolve(_message); +}); diff --git a/src/server/api/models/meta.ts b/src/server/api/models/meta.ts new file mode 100644 index 0000000000..ee1ada18fa --- /dev/null +++ b/src/server/api/models/meta.ts @@ -0,0 +1,7 @@ +import db from '../../../db/mongodb'; + +export default db.get('meta') as any; // fuck type definition + +export type IMeta = { + top_image: string; +}; diff --git a/src/server/api/models/mute.ts b/src/server/api/models/mute.ts new file mode 100644 index 0000000000..02f652c30b --- /dev/null +++ b/src/server/api/models/mute.ts @@ -0,0 +1,3 @@ +import db from '../../../db/mongodb'; + +export default db.get('mute') as any; // fuck type definition diff --git a/src/server/api/models/notification.ts b/src/server/api/models/notification.ts new file mode 100644 index 0000000000..bcb25534dc --- /dev/null +++ b/src/server/api/models/notification.ts @@ -0,0 +1,107 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../../../db/mongodb'; +import { IUser, pack as packUser } from './user'; +import { pack as packPost } from './post'; + +const Notification = db.get('notifications'); +export default Notification; + +export interface INotification { + _id: mongo.ObjectID; + created_at: Date; + + /** + * 通知の受信者 + */ + notifiee?: IUser; + + /** + * 通知の受信者 + */ + notifiee_id: mongo.ObjectID; + + /** + * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー + */ + notifier?: IUser; + + /** + * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー + */ + notifier_id: mongo.ObjectID; + + /** + * 通知の種類。 + * follow - フォローされた + * mention - 投稿で自分が言及された + * reply - (自分または自分がWatchしている)投稿が返信された + * repost - (自分または自分がWatchしている)投稿がRepostされた + * quote - (自分または自分がWatchしている)投稿が引用Repostされた + * reaction - (自分または自分がWatchしている)投稿にリアクションされた + * poll_vote - (自分または自分がWatchしている)投稿の投票に投票された + */ + type: 'follow' | 'mention' | 'reply' | 'repost' | 'quote' | 'reaction' | 'poll_vote'; + + /** + * 通知が読まれたかどうか + */ + is_read: Boolean; +} + +/** + * Pack a notification for API response + * + * @param {any} notification + * @return {Promise} + */ +export const pack = (notification: any) => new Promise(async (resolve, reject) => { + let _notification: any; + + // Populate the notification if 'notification' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(notification)) { + _notification = await Notification.findOne({ + _id: notification + }); + } else if (typeof notification === 'string') { + _notification = await Notification.findOne({ + _id: new mongo.ObjectID(notification) + }); + } else { + _notification = deepcopy(notification); + } + + // Rename _id to id + _notification.id = _notification._id; + delete _notification._id; + + // Rename notifier_id to user_id + _notification.user_id = _notification.notifier_id; + delete _notification.notifier_id; + + const me = _notification.notifiee_id; + delete _notification.notifiee_id; + + // Populate notifier + _notification.user = await packUser(_notification.user_id, me); + + switch (_notification.type) { + case 'follow': + // nope + break; + case 'mention': + case 'reply': + case 'repost': + case 'quote': + case 'reaction': + case 'poll_vote': + // Populate post + _notification.post = await packPost(_notification.post_id, me); + break; + default: + console.error(`Unknown type: ${_notification.type}`); + break; + } + + resolve(_notification); +}); diff --git a/src/server/api/models/othello-game.ts b/src/server/api/models/othello-game.ts new file mode 100644 index 0000000000..97508e46da --- /dev/null +++ b/src/server/api/models/othello-game.ts @@ -0,0 +1,109 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../../../db/mongodb'; +import { IUser, pack as packUser } from './user'; + +const Game = db.get('othello_games'); +export default Game; + +export interface IGame { + _id: mongo.ObjectID; + created_at: Date; + started_at: Date; + user1_id: mongo.ObjectID; + user2_id: mongo.ObjectID; + user1_accepted: boolean; + user2_accepted: boolean; + + /** + * どちらのプレイヤーが先行(黒)か + * 1 ... user1 + * 2 ... user2 + */ + black: number; + + is_started: boolean; + is_ended: boolean; + winner_id: mongo.ObjectID; + logs: Array<{ + at: Date; + color: boolean; + pos: number; + }>; + settings: { + map: string[]; + bw: string | number; + is_llotheo: boolean; + can_put_everywhere: boolean; + looped_board: boolean; + }; + form1: any; + form2: any; + + // ログのposを文字列としてすべて連結したもののCRC32値 + crc32: string; +} + +/** + * Pack an othello game for API response + */ +export const pack = ( + game: any, + me?: string | mongo.ObjectID | IUser, + options?: { + detail?: boolean + } +) => new Promise(async (resolve, reject) => { + const opts = Object.assign({ + detail: true + }, options); + + let _game: any; + + // Populate the game if 'game' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(game)) { + _game = await Game.findOne({ + _id: game + }); + } else if (typeof game === 'string') { + _game = await Game.findOne({ + _id: new mongo.ObjectID(game) + }); + } else { + _game = deepcopy(game); + } + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + // Rename _id to id + _game.id = _game._id; + delete _game._id; + + if (opts.detail === false) { + delete _game.logs; + delete _game.settings.map; + } else { + // 互換性のため + if (_game.settings.map.hasOwnProperty('size')) { + _game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g')); + } + } + + // Populate user + _game.user1 = await packUser(_game.user1_id, meId); + _game.user2 = await packUser(_game.user2_id, meId); + if (_game.winner_id) { + _game.winner = await packUser(_game.winner_id, meId); + } else { + _game.winner = null; + } + + resolve(_game); +}); diff --git a/src/server/api/models/othello-matching.ts b/src/server/api/models/othello-matching.ts new file mode 100644 index 0000000000..3c29e6a00c --- /dev/null +++ b/src/server/api/models/othello-matching.ts @@ -0,0 +1,44 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../../../db/mongodb'; +import { IUser, pack as packUser } from './user'; + +const Matching = db.get('othello_matchings'); +export default Matching; + +export interface IMatching { + _id: mongo.ObjectID; + created_at: Date; + parent_id: mongo.ObjectID; + child_id: mongo.ObjectID; +} + +/** + * Pack an othello matching for API response + */ +export const pack = ( + matching: any, + me?: string | mongo.ObjectID | IUser +) => new Promise(async (resolve, reject) => { + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + const _matching = deepcopy(matching); + + // Rename _id to id + _matching.id = _matching._id; + delete _matching._id; + + // Populate user + _matching.parent = await packUser(_matching.parent_id, meId); + _matching.child = await packUser(_matching.child_id, meId); + + resolve(_matching); +}); diff --git a/src/server/api/models/poll-vote.ts b/src/server/api/models/poll-vote.ts new file mode 100644 index 0000000000..c6638ccf1c --- /dev/null +++ b/src/server/api/models/poll-vote.ts @@ -0,0 +1,3 @@ +import db from '../../../db/mongodb'; + +export default db.get('poll_votes') as any; // fuck type definition diff --git a/src/server/api/models/post-reaction.ts b/src/server/api/models/post-reaction.ts new file mode 100644 index 0000000000..5cd122d76b --- /dev/null +++ b/src/server/api/models/post-reaction.ts @@ -0,0 +1,51 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../../../db/mongodb'; +import Reaction from './post-reaction'; +import { pack as packUser } from './user'; + +const PostReaction = db.get('post_reactions'); +export default PostReaction; + +export interface IPostReaction { + _id: mongo.ObjectID; + created_at: Date; + deleted_at: Date; + reaction: string; +} + +/** + * Pack a reaction for API response + * + * @param {any} reaction + * @param {any} me? + * @return {Promise} + */ +export const pack = ( + reaction: any, + me?: any +) => new Promise(async (resolve, reject) => { + let _reaction: any; + + // Populate the reaction if 'reaction' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(reaction)) { + _reaction = await Reaction.findOne({ + _id: reaction + }); + } else if (typeof reaction === 'string') { + _reaction = await Reaction.findOne({ + _id: new mongo.ObjectID(reaction) + }); + } else { + _reaction = deepcopy(reaction); + } + + // Rename _id to id + _reaction.id = _reaction._id; + delete _reaction._id; + + // Populate user + _reaction.user = await packUser(_reaction.user_id, me); + + resolve(_reaction); +}); diff --git a/src/server/api/models/post-watching.ts b/src/server/api/models/post-watching.ts new file mode 100644 index 0000000000..9a4163c8dc --- /dev/null +++ b/src/server/api/models/post-watching.ts @@ -0,0 +1,3 @@ +import db from '../../../db/mongodb'; + +export default db.get('post_watching') as any; // fuck type definition diff --git a/src/server/api/models/post.ts b/src/server/api/models/post.ts new file mode 100644 index 0000000000..3f648e08cd --- /dev/null +++ b/src/server/api/models/post.ts @@ -0,0 +1,219 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import rap from '@prezzemolo/rap'; +import db from '../../../db/mongodb'; +import { IUser, pack as packUser } from './user'; +import { pack as packApp } from './app'; +import { pack as packChannel } from './channel'; +import Vote from './poll-vote'; +import Reaction from './post-reaction'; +import { pack as packFile } from './drive-file'; +import parse from '../common/text'; + +const Post = db.get('posts'); + +export default Post; + +export function isValidText(text: string): boolean { + return text.length <= 1000 && text.trim() != ''; +} + +export type IPost = { + _id: mongo.ObjectID; + channel_id: mongo.ObjectID; + created_at: Date; + media_ids: mongo.ObjectID[]; + reply_id: mongo.ObjectID; + repost_id: mongo.ObjectID; + poll: any; // todo + text: string; + user_id: mongo.ObjectID; + app_id: mongo.ObjectID; + category: string; + is_category_verified: boolean; + via_mobile: boolean; + geo: { + latitude: number; + longitude: number; + altitude: number; + accuracy: number; + altitudeAccuracy: number; + heading: number; + speed: number; + }; +}; + +/** + * Pack a post for API response + * + * @param post target + * @param me? serializee + * @param options? serialize options + * @return response + */ +export const pack = async ( + post: string | mongo.ObjectID | IPost, + me?: string | mongo.ObjectID | IUser, + options?: { + detail: boolean + } +) => { + const opts = options || { + detail: true, + }; + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + let _post: any; + + // Populate the post if 'post' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(post)) { + _post = await Post.findOne({ + _id: post + }); + } else if (typeof post === 'string') { + _post = await Post.findOne({ + _id: new mongo.ObjectID(post) + }); + } else { + _post = deepcopy(post); + } + + if (!_post) throw 'invalid post arg.'; + + const id = _post._id; + + // Rename _id to id + _post.id = _post._id; + delete _post._id; + + delete _post.mentions; + + // Parse text + if (_post.text) { + _post.ast = parse(_post.text); + } + + // Populate user + _post.user = packUser(_post.user_id, meId); + + // Populate app + if (_post.app_id) { + _post.app = packApp(_post.app_id); + } + + // Populate channel + if (_post.channel_id) { + _post.channel = packChannel(_post.channel_id); + } + + // Populate media + if (_post.media_ids) { + _post.media = Promise.all(_post.media_ids.map(fileId => + packFile(fileId) + )); + } + + // When requested a detailed post data + if (opts.detail) { + // Get previous post info + _post.prev = (async () => { + const prev = await Post.findOne({ + user_id: _post.user_id, + _id: { + $lt: id + } + }, { + fields: { + _id: true + }, + sort: { + _id: -1 + } + }); + return prev ? prev._id : null; + })(); + + // Get next post info + _post.next = (async () => { + const next = await Post.findOne({ + user_id: _post.user_id, + _id: { + $gt: id + } + }, { + fields: { + _id: true + }, + sort: { + _id: 1 + } + }); + return next ? next._id : null; + })(); + + if (_post.reply_id) { + // Populate reply to post + _post.reply = pack(_post.reply_id, meId, { + detail: false + }); + } + + if (_post.repost_id) { + // Populate repost + _post.repost = pack(_post.repost_id, meId, { + detail: _post.text == null + }); + } + + // Poll + if (meId && _post.poll) { + _post.poll = (async (poll) => { + const vote = await Vote + .findOne({ + user_id: meId, + post_id: id + }); + + if (vote != null) { + const myChoice = poll.choices + .filter(c => c.id == vote.choice)[0]; + + myChoice.is_voted = true; + } + + return poll; + })(_post.poll); + } + + // Fetch my reaction + if (meId) { + _post.my_reaction = (async () => { + const reaction = await Reaction + .findOne({ + user_id: meId, + post_id: id, + deleted_at: { $exists: false } + }); + + if (reaction) { + return reaction.reaction; + } + + return null; + })(); + } + } + + // resolve promises in _post object + _post = await rap(_post); + + return _post; +}; diff --git a/src/server/api/models/signin.ts b/src/server/api/models/signin.ts new file mode 100644 index 0000000000..5cffb3c310 --- /dev/null +++ b/src/server/api/models/signin.ts @@ -0,0 +1,29 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../../../db/mongodb'; + +const Signin = db.get('signin'); +export default Signin; + +export interface ISignin { + _id: mongo.ObjectID; +} + +/** + * Pack a signin record for API response + * + * @param {any} record + * @return {Promise} + */ +export const pack = ( + record: any +) => new Promise(async (resolve, reject) => { + + const _record = deepcopy(record); + + // Rename _id to id + _record.id = _record._id; + delete _record._id; + + resolve(_record); +}); diff --git a/src/server/api/models/sw-subscription.ts b/src/server/api/models/sw-subscription.ts new file mode 100644 index 0000000000..4506a982f2 --- /dev/null +++ b/src/server/api/models/sw-subscription.ts @@ -0,0 +1,3 @@ +import db from '../../../db/mongodb'; + +export default db.get('sw_subscriptions') as any; // fuck type definition diff --git a/src/server/api/models/user.ts b/src/server/api/models/user.ts new file mode 100644 index 0000000000..8e7d50baa3 --- /dev/null +++ b/src/server/api/models/user.ts @@ -0,0 +1,340 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import rap from '@prezzemolo/rap'; +import db from '../../../db/mongodb'; +import { IPost, pack as packPost } from './post'; +import Following from './following'; +import Mute from './mute'; +import getFriends from '../common/get-friends'; +import config from '../../../conf'; + +const User = db.get('users'); + +User.createIndex('username'); +User.createIndex('account.token'); + +export default User; + +export function validateUsername(username: string): boolean { + return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username); +} + +export function validatePassword(password: string): boolean { + return typeof password == 'string' && password != ''; +} + +export function isValidName(name: string): boolean { + return typeof name == 'string' && name.length < 30 && name.trim() != ''; +} + +export function isValidDescription(description: string): boolean { + return typeof description == 'string' && description.length < 500 && description.trim() != ''; +} + +export function isValidLocation(location: string): boolean { + return typeof location == 'string' && location.length < 50 && location.trim() != ''; +} + +export function isValidBirthday(birthday: string): boolean { + return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday); +} + +export type ILocalAccount = { + keypair: string; + email: string; + links: string[]; + password: string; + token: string; + twitter: { + access_token: string; + access_token_secret: string; + user_id: string; + screen_name: string; + }; + line: { + user_id: string; + }; + profile: { + location: string; + birthday: string; // 'YYYY-MM-DD' + tags: string[]; + }; + last_used_at: Date; + is_bot: boolean; + is_pro: boolean; + two_factor_secret: string; + two_factor_enabled: boolean; + client_settings: any; + settings: any; +}; + +export type IRemoteAccount = { + uri: string; +}; + +export type IUser = { + _id: mongo.ObjectID; + created_at: Date; + deleted_at: Date; + followers_count: number; + following_count: number; + name: string; + posts_count: number; + drive_capacity: number; + username: string; + username_lower: string; + avatar_id: mongo.ObjectID; + banner_id: mongo.ObjectID; + data: any; + description: string; + latest_post: IPost; + pinned_post_id: mongo.ObjectID; + is_suspended: boolean; + keywords: string[]; + host: string; + host_lower: string; + account: ILocalAccount | IRemoteAccount; +}; + +export function init(user): IUser { + user._id = new mongo.ObjectID(user._id); + user.avatar_id = new mongo.ObjectID(user.avatar_id); + user.banner_id = new mongo.ObjectID(user.banner_id); + user.pinned_post_id = new mongo.ObjectID(user.pinned_post_id); + return user; +} + +/** + * Pack a user for API response + * + * @param user target + * @param me? serializee + * @param options? serialize options + * @return Packed user + */ +export const pack = ( + user: string | mongo.ObjectID | IUser, + me?: string | mongo.ObjectID | IUser, + options?: { + detail?: boolean, + includeSecrets?: boolean + } +) => new Promise(async (resolve, reject) => { + + const opts = Object.assign({ + detail: false, + includeSecrets: false + }, options); + + let _user: any; + + const fields = opts.detail ? { + } : { + 'account.settings': false, + 'account.client_settings': false, + 'account.profile': false, + 'account.keywords': false, + 'account.domains': false + }; + + // Populate the user if 'user' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(user)) { + _user = await User.findOne({ + _id: user + }, { fields }); + } else if (typeof user === 'string') { + _user = await User.findOne({ + _id: new mongo.ObjectID(user) + }, { fields }); + } else { + _user = deepcopy(user); + } + + if (!_user) return reject('invalid user arg.'); + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + // Rename _id to id + _user.id = _user._id; + delete _user._id; + + // Remove needless properties + delete _user.latest_post; + + if (!_user.host) { + // Remove private properties + delete _user.account.keypair; + delete _user.account.password; + delete _user.account.token; + delete _user.account.two_factor_temp_secret; + delete _user.account.two_factor_secret; + delete _user.username_lower; + if (_user.account.twitter) { + delete _user.account.twitter.access_token; + delete _user.account.twitter.access_token_secret; + } + delete _user.account.line; + + // Visible via only the official client + if (!opts.includeSecrets) { + delete _user.account.email; + delete _user.account.settings; + delete _user.account.client_settings; + } + + if (!opts.detail) { + delete _user.account.two_factor_enabled; + } + } + + _user.avatar_url = _user.avatar_id != null + ? `${config.drive_url}/${_user.avatar_id}` + : `${config.drive_url}/default-avatar.jpg`; + + _user.banner_url = _user.banner_id != null + ? `${config.drive_url}/${_user.banner_id}` + : null; + + if (!meId || !meId.equals(_user.id) || !opts.detail) { + delete _user.avatar_id; + delete _user.banner_id; + + delete _user.drive_capacity; + } + + if (meId && !meId.equals(_user.id)) { + // Whether the user is following + _user.is_following = (async () => { + const follow = await Following.findOne({ + follower_id: meId, + followee_id: _user.id, + deleted_at: { $exists: false } + }); + return follow !== null; + })(); + + // Whether the user is followed + _user.is_followed = (async () => { + const follow2 = await Following.findOne({ + follower_id: _user.id, + followee_id: meId, + deleted_at: { $exists: false } + }); + return follow2 !== null; + })(); + + // Whether the user is muted + _user.is_muted = (async () => { + const mute = await Mute.findOne({ + muter_id: meId, + mutee_id: _user.id, + deleted_at: { $exists: false } + }); + return mute !== null; + })(); + } + + if (opts.detail) { + if (_user.pinned_post_id) { + // Populate pinned post + _user.pinned_post = packPost(_user.pinned_post_id, meId, { + detail: true + }); + } + + if (meId && !meId.equals(_user.id)) { + const myFollowingIds = await getFriends(meId); + + // Get following you know count + _user.following_you_know_count = Following.count({ + followee_id: { $in: myFollowingIds }, + follower_id: _user.id, + deleted_at: { $exists: false } + }); + + // Get followers you know count + _user.followers_you_know_count = Following.count({ + followee_id: _user.id, + follower_id: { $in: myFollowingIds }, + deleted_at: { $exists: false } + }); + } + } + + // resolve promises in _user object + _user = await rap(_user); + + resolve(_user); +}); + +/** + * Pack a user for ActivityPub + * + * @param user target + * @return Packed user + */ +export const packForAp = ( + user: string | mongo.ObjectID | IUser +) => new Promise(async (resolve, reject) => { + + let _user: any; + + const fields = { + // something + }; + + // Populate the user if 'user' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(user)) { + _user = await User.findOne({ + _id: user + }, { fields }); + } else if (typeof user === 'string') { + _user = await User.findOne({ + _id: new mongo.ObjectID(user) + }, { fields }); + } else { + _user = deepcopy(user); + } + + if (!_user) return reject('invalid user arg.'); + + const userUrl = `${config.url}/@${_user.username}`; + + resolve({ + "@context": ["https://www.w3.org/ns/activitystreams", { + "@language": "ja" + }], + "type": "Person", + "id": userUrl, + "following": `${userUrl}/following.json`, + "followers": `${userUrl}/followers.json`, + "liked": `${userUrl}/liked.json`, + "inbox": `${userUrl}/inbox.json`, + "outbox": `${userUrl}/outbox.json`, + "preferredUsername": _user.username, + "name": _user.name, + "summary": _user.description, + "icon": [ + `${config.drive_url}/${_user.avatar_id}` + ] + }); +}); + +/* +function img(url) { + return { + thumbnail: { + large: `${url}`, + medium: '', + small: '' + } + }; +} +*/ diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts new file mode 100644 index 0000000000..bbc9908991 --- /dev/null +++ b/src/server/api/private/signin.ts @@ -0,0 +1,91 @@ +import * as express from 'express'; +import * as bcrypt from 'bcryptjs'; +import * as speakeasy from 'speakeasy'; +import { default as User, ILocalAccount, IUser } from '../models/user'; +import Signin, { pack } from '../models/signin'; +import event from '../event'; +import signin from '../common/signin'; +import config from '../../../conf'; + +export default async (req: express.Request, res: express.Response) => { + res.header('Access-Control-Allow-Origin', config.url); + res.header('Access-Control-Allow-Credentials', 'true'); + + const username = req.body['username']; + const password = req.body['password']; + const token = req.body['token']; + + if (typeof username != 'string') { + res.sendStatus(400); + return; + } + + if (typeof password != 'string') { + res.sendStatus(400); + return; + } + + if (token != null && typeof token != 'string') { + res.sendStatus(400); + return; + } + + // Fetch user + const user: IUser = await User.findOne({ + username_lower: username.toLowerCase(), + host: null + }, { + fields: { + data: false, + 'account.profile': false + } + }); + + if (user === null) { + res.status(404).send({ + error: 'user not found' + }); + return; + } + + const account = user.account as ILocalAccount; + + // Compare password + const same = await bcrypt.compare(password, account.password); + + if (same) { + if (account.two_factor_enabled) { + const verified = (speakeasy as any).totp.verify({ + secret: account.two_factor_secret, + encoding: 'base32', + token: token + }); + + if (verified) { + signin(res, user, false); + } else { + res.status(400).send({ + error: 'invalid token' + }); + } + } else { + signin(res, user, false); + } + } else { + res.status(400).send({ + error: 'incorrect password' + }); + } + + // Append signin history + const record = await Signin.insert({ + created_at: new Date(), + user_id: user._id, + ip: req.ip, + headers: req.headers, + success: same + }); + + // Publish signin event + event(user._id, 'signin', await pack(record)); +}; diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts new file mode 100644 index 0000000000..9f55393313 --- /dev/null +++ b/src/server/api/private/signup.ts @@ -0,0 +1,165 @@ +import * as uuid from 'uuid'; +import * as express from 'express'; +import * as bcrypt from 'bcryptjs'; +import { generate as generateKeypair } from '../../../crypto_key'; +import recaptcha = require('recaptcha-promise'); +import User, { IUser, validateUsername, validatePassword, pack } from '../models/user'; +import generateUserToken from '../common/generate-native-user-token'; +import config from '../../../conf'; + +recaptcha.init({ + secret_key: config.recaptcha.secret_key +}); + +const home = { + left: [ + 'profile', + 'calendar', + 'activity', + 'rss', + 'trends', + 'photo-stream', + 'version' + ], + right: [ + 'broadcast', + 'notifications', + 'users', + 'polls', + 'server', + 'donation', + 'nav', + 'tips' + ] +}; + +export default async (req: express.Request, res: express.Response) => { + // Verify recaptcha + // ただしテスト時はこの機構は障害となるため無効にする + if (process.env.NODE_ENV !== 'test') { + const success = await recaptcha(req.body['g-recaptcha-response']); + + if (!success) { + res.status(400).send('recaptcha-failed'); + return; + } + } + + const username = req.body['username']; + const password = req.body['password']; + const name = '名無し'; + + // Validate username + if (!validateUsername(username)) { + res.sendStatus(400); + return; + } + + // Validate password + if (!validatePassword(password)) { + res.sendStatus(400); + return; + } + + // Fetch exist user that same username + const usernameExist = await User + .count({ + username_lower: username.toLowerCase(), + host: null + }, { + limit: 1 + }); + + // Check username already used + if (usernameExist !== 0) { + res.sendStatus(400); + return; + } + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); + + // Generate secret + const secret = generateUserToken(); + + //#region Construct home data + const homeData = []; + + home.left.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'left', + data: {} + }); + }); + + home.right.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'right', + data: {} + }); + }); + //#endregion + + // Create account + const account: IUser = await User.insert({ + avatar_id: null, + banner_id: null, + created_at: new Date(), + description: null, + followers_count: 0, + following_count: 0, + name: name, + posts_count: 0, + likes_count: 0, + liked_count: 0, + drive_capacity: 1073741824, // 1GB + username: username, + username_lower: username.toLowerCase(), + host: null, + host_lower: 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: { + auto_watch: true + }, + client_settings: { + 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/reply.ts b/src/server/api/reply.ts new file mode 100644 index 0000000000..e47fc85b9b --- /dev/null +++ b/src/server/api/reply.ts @@ -0,0 +1,13 @@ +import * as express from 'express'; + +export default (res: express.Response, x?: any, y?: any) => { + if (x === undefined) { + res.sendStatus(204); + } else if (typeof x === 'number') { + res.status(x).send({ + error: x === 500 ? 'INTERNAL_ERROR' : y + }); + } else { + res.send(x); + } +}; diff --git a/src/server/api/server.ts b/src/server/api/server.ts new file mode 100644 index 0000000000..e89d196096 --- /dev/null +++ b/src/server/api/server.ts @@ -0,0 +1,55 @@ +/** + * API Server + */ + +import * as express from 'express'; +import * as bodyParser from 'body-parser'; +import * as cors from 'cors'; +import * as multer from 'multer'; + +// import authenticate from './authenticate'; +import endpoints from './endpoints'; + +/** + * Init app + */ +const app = express(); + +app.disable('x-powered-by'); +app.set('etag', false); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json({ + type: ['application/json', 'text/plain'], + verify: (req, res, buf, encoding) => { + if (buf && buf.length) { + (req as any).rawBody = buf.toString(encoding || 'utf8'); + } + } +})); +app.use(cors()); + +app.get('/', (req, res) => { + res.send('YEE HAW'); +}); + +/** + * Register endpoint handlers + */ +endpoints.forEach(endpoint => + endpoint.withFile ? + app.post(`/${endpoint.name}`, + endpoint.withFile ? multer({ storage: multer.diskStorage({}) }).single('file') : null, + require('./api-handler').default.bind(null, endpoint)) : + app.post(`/${endpoint.name}`, + require('./api-handler').default.bind(null, endpoint)) +); + +app.post('/signup', require('./private/signup').default); +app.post('/signin', require('./private/signin').default); + +require('./service/github')(app); +require('./service/twitter')(app); + +require('./bot/interfaces/line')(app); + +module.exports = app; diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts new file mode 100644 index 0000000000..a33d359753 --- /dev/null +++ b/src/server/api/service/github.ts @@ -0,0 +1,124 @@ +import * as EventEmitter from 'events'; +import * as express from 'express'; +const crypto = require('crypto'); +import User from '../models/user'; +import config from '../../../conf'; +import queue from '../../../queue'; + +module.exports = async (app: express.Application) => { + if (config.github_bot == null) return; + + const bot = await User.findOne({ + username_lower: config.github_bot.username.toLowerCase() + }); + + if (bot == null) { + console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`); + return; + } + + const post = text => require('../endpoints/posts/create')({ text }, bot); + + const handler = new EventEmitter(); + + app.post('/hooks/github', (req, res, next) => { + // 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')}`))) { + handler.emit(req.headers['x-github-event'] as string, req.body); + res.sendStatus(200); +// } else { +// res.sendStatus(400); +// } + }); + + handler.on('status', event => { + const state = event.state; + switch (state) { + case 'error': + case 'failure': + const commit = event.commit; + const parent = commit.parents[0]; + + queue.create('gitHubFailureReport', { + userId: bot._id, + parentUrl: parent.url, + htmlUrl: commit.html_url, + message: commit.commit.message, + }).save(); + break; + } + }); + + handler.on('push', event => { + const ref = event.ref; + switch (ref) { + case 'refs/heads/master': + const pusher = event.pusher; + const compare = event.compare; + const commits = event.commits; + post([ + `Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`, + commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'), + ].join('\n')); + break; + case 'refs/heads/release': + const commit = event.commits[0]; + post(`RELEASED: ${commit.message}`); + break; + } + }); + + handler.on('issues', event => { + const issue = event.issue; + const action = event.action; + let title: string; + switch (action) { + case 'opened': title = 'Issue opened'; break; + case 'closed': title = 'Issue closed'; break; + case 'reopened': title = 'Issue reopened'; break; + default: return; + } + post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`); + }); + + handler.on('issue_comment', event => { + const issue = event.issue; + const comment = event.comment; + const action = event.action; + let text: string; + switch (action) { + case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break; + default: return; + } + post(text); + }); + + handler.on('watch', event => { + const sender = event.sender; + post(`⭐️ Starred by **${sender.login}** ⭐️`); + }); + + handler.on('fork', event => { + const repo = event.forkee; + post(`🍴 Forked:\n${repo.html_url} 🍴`); + }); + + handler.on('pull_request', event => { + const pr = event.pull_request; + const action = event.action; + let text: string; + switch (action) { + case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break; + case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break; + case 'closed': + text = pr.merged + ? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}` + : `Pull Request Closed:「${pr.title}」\n${pr.html_url}`; + break; + default: return; + } + post(text); + }); +}; diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts new file mode 100644 index 0000000000..861f63ed67 --- /dev/null +++ b/src/server/api/service/twitter.ts @@ -0,0 +1,176 @@ +import * as express from 'express'; +import * as cookie from 'cookie'; +import * as uuid from 'uuid'; +// import * as Twitter from 'twitter'; +// const Twitter = require('twitter'); +import autwh from 'autwh'; +import redis from '../../../db/redis'; +import User, { pack } from '../models/user'; +import event from '../event'; +import config from '../../../conf'; +import signin from '../common/signin'; + +module.exports = (app: express.Application) => { + function getUserToken(req: express.Request) { + // req.headers['cookie'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + return ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1]; + } + + function compareOrigin(req: express.Request) { + function normalizeUrl(url: string) { + return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; + } + + // req.headers['referer'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + const referer = req.headers['referer'] as string; + + return (normalizeUrl(referer) == normalizeUrl(config.url)); + } + + app.get('/disconnect/twitter', async (req, res): Promise => { + if (!compareOrigin(req)) { + res.status(400).send('invalid origin'); + return; + } + + const userToken = getUserToken(req); + if (userToken == null) return res.send('plz signin'); + + const user = await User.findOneAndUpdate({ + host: null, + 'account.token': userToken + }, { + $set: { + 'account.twitter': null + } + }); + + res.send(`Twitterの連携を解除しました :v:`); + + // Publish i updated event + event(user._id, 'i_updated', await pack(user, user, { + detail: true, + includeSecrets: true + })); + }); + + if (config.twitter == null) { + app.get('/connect/twitter', (req, res) => { + res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); + }); + + app.get('/signin/twitter', (req, res) => { + res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); + }); + + return; + } + + const twAuth = autwh({ + consumerKey: config.twitter.consumer_key, + consumerSecret: config.twitter.consumer_secret, + callbackUrl: `${config.api_url}/tw/cb` + }); + + app.get('/connect/twitter', async (req, res): Promise => { + if (!compareOrigin(req)) { + res.status(400).send('invalid origin'); + return; + } + + const userToken = getUserToken(req); + if (userToken == null) return res.send('plz signin'); + + const ctx = await twAuth.begin(); + redis.set(userToken, JSON.stringify(ctx)); + res.redirect(ctx.url); + }); + + app.get('/signin/twitter', async (req, res): Promise => { + const ctx = await twAuth.begin(); + + const sessid = uuid(); + + redis.set(sessid, JSON.stringify(ctx)); + + const expires = 1000 * 60 * 60; // 1h + res.cookie('signin_with_twitter_session_id', sessid, { + path: '/', + domain: `.${config.host}`, + secure: config.url.substr(0, 5) === 'https', + httpOnly: true, + expires: new Date(Date.now() + expires), + maxAge: expires + }); + + res.redirect(ctx.url); + }); + + app.get('/tw/cb', (req, res): any => { + const userToken = getUserToken(req); + + if (userToken == null) { + // req.headers['cookie'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + const cookies = cookie.parse((req.headers['cookie'] as string || '')); + + const sessid = cookies['signin_with_twitter_session_id']; + + if (sessid == undefined) { + res.status(400).send('invalid session'); + return; + } + + redis.get(sessid, async (_, ctx) => { + const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); + + const user = await User.findOne({ + host: null, + 'account.twitter.user_id': result.userId + }); + + if (user == null) { + res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); + return; + } + + signin(res, user, true); + }); + } else { + const verifier = req.query.oauth_verifier; + + if (verifier == null) { + res.status(400).send('invalid session'); + return; + } + + redis.get(userToken, async (_, ctx) => { + const result = await twAuth.done(JSON.parse(ctx), verifier); + + const user = await User.findOneAndUpdate({ + host: null, + 'account.token': userToken + }, { + $set: { + 'account.twitter': { + access_token: result.accessToken, + access_token_secret: result.accessTokenSecret, + user_id: result.userId, + screen_name: result.screenName + } + } + }); + + res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); + + // Publish i updated event + event(user._id, 'i_updated', await pack(user, user, { + detail: true, + includeSecrets: true + })); + }); + } + }); +}; diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts new file mode 100644 index 0000000000..d67d77cbf4 --- /dev/null +++ b/src/server/api/stream/channel.ts @@ -0,0 +1,12 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void { + const channel = request.resourceURL.query.channel; + + // Subscribe channel stream + subscriber.subscribe(`misskey:channel-stream:${channel}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); +} diff --git a/src/server/api/stream/drive.ts b/src/server/api/stream/drive.ts new file mode 100644 index 0000000000..c97ab80dcc --- /dev/null +++ b/src/server/api/stream/drive.ts @@ -0,0 +1,10 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { + // Subscribe drive stream + subscriber.subscribe(`misskey:drive-stream:${user._id}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); +} diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts new file mode 100644 index 0000000000..1ef0f33b4b --- /dev/null +++ b/src/server/api/stream/home.ts @@ -0,0 +1,95 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import * as debug from 'debug'; + +import User from '../models/user'; +import Mute from '../models/mute'; +import { pack as packPost } from '../models/post'; +import readNotification from '../common/read-notification'; + +const log = debug('misskey'); + +export default async function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any) { + // Subscribe Home stream channel + subscriber.subscribe(`misskey:user-stream:${user._id}`); + + const mute = await Mute.find({ + muter_id: user._id, + deleted_at: { $exists: false } + }); + const mutedUserIds = mute.map(m => m.mutee_id.toString()); + + subscriber.on('message', async (channel, data) => { + switch (channel.split(':')[1]) { + case 'user-stream': + try { + const x = JSON.parse(data); + + if (x.type == 'post') { + if (mutedUserIds.indexOf(x.body.user_id) != -1) { + return; + } + if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.user_id) != -1) { + return; + } + if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.user_id) != -1) { + return; + } + } else if (x.type == 'notification') { + if (mutedUserIds.indexOf(x.body.user_id) != -1) { + return; + } + } + + connection.send(data); + } catch (e) { + connection.send(data); + } + break; + case 'post-stream': + const postId = channel.split(':')[2]; + log(`RECEIVED: ${postId} ${data} by @${user.username}`); + const post = await packPost(postId, user, { + detail: true + }); + connection.send(JSON.stringify({ + type: 'post-updated', + body: { + post: post + } + })); + break; + } + }); + + connection.on('message', data => { + const msg = JSON.parse(data.utf8Data); + + switch (msg.type) { + case 'api': + // TODO + break; + + case 'alive': + // Update lastUsedAt + User.update({ _id: user._id }, { + $set: { + 'account.last_used_at': new Date() + } + }); + break; + + case 'read_notification': + if (!msg.id) return; + readNotification(user._id, msg.id); + break; + + case 'capture': + if (!msg.id) return; + const postId = msg.id; + log(`CAPTURE: ${postId} by @${user.username}`); + subscriber.subscribe(`misskey:post-stream:${postId}`); + break; + } + }); +} diff --git a/src/server/api/stream/messaging-index.ts b/src/server/api/stream/messaging-index.ts new file mode 100644 index 0000000000..c1b2fbc806 --- /dev/null +++ b/src/server/api/stream/messaging-index.ts @@ -0,0 +1,10 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { + // Subscribe messaging index stream + subscriber.subscribe(`misskey:messaging-index-stream:${user._id}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); +} diff --git a/src/server/api/stream/messaging.ts b/src/server/api/stream/messaging.ts new file mode 100644 index 0000000000..a4a12426a3 --- /dev/null +++ b/src/server/api/stream/messaging.ts @@ -0,0 +1,24 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import read from '../common/read-messaging-message'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { + const otherparty = request.resourceURL.query.otherparty; + + // Subscribe messaging stream + subscriber.subscribe(`misskey:messaging-stream:${user._id}-${otherparty}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); + + connection.on('message', async (data) => { + const msg = JSON.parse(data.utf8Data); + + switch (msg.type) { + case 'read': + if (!msg.id) return; + read(user._id, otherparty, msg.id); + break; + } + }); +} diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts new file mode 100644 index 0000000000..1c846f27ae --- /dev/null +++ b/src/server/api/stream/othello-game.ts @@ -0,0 +1,331 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import * as CRC32 from 'crc-32'; +import Game, { pack } from '../models/othello-game'; +import { publishOthelloGameStream } from '../event'; +import Othello from '../../common/othello/core'; +import * as maps from '../../common/othello/maps'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user?: any): void { + const gameId = request.resourceURL.query.game; + + // Subscribe game stream + subscriber.subscribe(`misskey:othello-game-stream:${gameId}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); + + connection.on('message', async (data) => { + const msg = JSON.parse(data.utf8Data); + + switch (msg.type) { + case 'accept': + accept(true); + break; + + case 'cancel-accept': + accept(false); + break; + + case 'update-settings': + if (msg.settings == null) return; + updateSettings(msg.settings); + break; + + case 'init-form': + if (msg.body == null) return; + initForm(msg.body); + break; + + case 'update-form': + if (msg.id == null || msg.value === undefined) return; + updateForm(msg.id, msg.value); + break; + + case 'message': + if (msg.body == null) return; + message(msg.body); + break; + + case 'set': + if (msg.pos == null) return; + set(msg.pos); + break; + + case 'check': + if (msg.crc32 == null) return; + check(msg.crc32); + break; + } + }); + + async function updateSettings(settings) { + const game = await Game.findOne({ _id: gameId }); + + if (game.is_started) return; + if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return; + if (game.user1_id.equals(user._id) && game.user1_accepted) return; + if (game.user2_id.equals(user._id) && game.user2_accepted) return; + + await Game.update({ _id: gameId }, { + $set: { + settings + } + }); + + publishOthelloGameStream(gameId, 'update-settings', settings); + } + + async function initForm(form) { + const game = await Game.findOne({ _id: gameId }); + + if (game.is_started) return; + if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return; + + const set = game.user1_id.equals(user._id) ? { + form1: form + } : { + form2: form + }; + + await Game.update({ _id: gameId }, { + $set: set + }); + + publishOthelloGameStream(gameId, 'init-form', { + user_id: user._id, + form + }); + } + + async function updateForm(id, value) { + const game = await Game.findOne({ _id: gameId }); + + if (game.is_started) return; + if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return; + + const form = game.user1_id.equals(user._id) ? game.form2 : game.form1; + + const item = form.find(i => i.id == id); + + if (item == null) return; + + item.value = value; + + const set = game.user1_id.equals(user._id) ? { + form2: form + } : { + form1: form + }; + + await Game.update({ _id: gameId }, { + $set: set + }); + + publishOthelloGameStream(gameId, 'update-form', { + user_id: user._id, + id, + value + }); + } + + async function message(message) { + message.id = Math.random(); + publishOthelloGameStream(gameId, 'message', { + user_id: user._id, + message + }); + } + + async function accept(accept: boolean) { + const game = await Game.findOne({ _id: gameId }); + + if (game.is_started) return; + + let bothAccepted = false; + + if (game.user1_id.equals(user._id)) { + await Game.update({ _id: gameId }, { + $set: { + user1_accepted: accept + } + }); + + publishOthelloGameStream(gameId, 'change-accepts', { + user1: accept, + user2: game.user2_accepted + }); + + if (accept && game.user2_accepted) bothAccepted = true; + } else if (game.user2_id.equals(user._id)) { + await Game.update({ _id: gameId }, { + $set: { + user2_accepted: accept + } + }); + + publishOthelloGameStream(gameId, 'change-accepts', { + user1: game.user1_accepted, + user2: accept + }); + + if (accept && game.user1_accepted) bothAccepted = true; + } else { + return; + } + + if (bothAccepted) { + // 3秒後、まだacceptされていたらゲーム開始 + setTimeout(async () => { + const freshGame = await Game.findOne({ _id: gameId }); + if (freshGame == null || freshGame.is_started || freshGame.is_ended) return; + if (!freshGame.user1_accepted || !freshGame.user2_accepted) return; + + let bw: number; + if (freshGame.settings.bw == 'random') { + bw = Math.random() > 0.5 ? 1 : 2; + } else { + bw = freshGame.settings.bw as number; + } + + function getRandomMap() { + const mapCount = Object.entries(maps).length; + const rnd = Math.floor(Math.random() * mapCount); + return Object.entries(maps).find((x, i) => i == rnd)[1].data; + } + + const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); + + await Game.update({ _id: gameId }, { + $set: { + started_at: new Date(), + is_started: true, + black: bw, + 'settings.map': map + } + }); + + //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 + const o = new Othello(map, { + isLlotheo: freshGame.settings.is_llotheo, + canPutEverywhere: freshGame.settings.can_put_everywhere, + loopedBoard: freshGame.settings.looped_board + }); + + if (o.isEnded) { + let winner; + if (o.winner === true) { + winner = freshGame.black == 1 ? freshGame.user1_id : freshGame.user2_id; + } else if (o.winner === false) { + winner = freshGame.black == 1 ? freshGame.user2_id : freshGame.user1_id; + } else { + winner = null; + } + + await Game.update({ + _id: gameId + }, { + $set: { + is_ended: true, + winner_id: winner + } + }); + + publishOthelloGameStream(gameId, 'ended', { + winner_id: winner, + game: await pack(gameId, user) + }); + } + //#endregion + + publishOthelloGameStream(gameId, 'started', await pack(gameId, user)); + }, 3000); + } + } + + // 石を打つ + async function set(pos) { + const game = await Game.findOne({ _id: gameId }); + + if (!game.is_started) return; + if (game.is_ended) return; + if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return; + + const o = new Othello(game.settings.map, { + isLlotheo: game.settings.is_llotheo, + canPutEverywhere: game.settings.can_put_everywhere, + loopedBoard: game.settings.looped_board + }); + + game.logs.forEach(log => { + o.put(log.color, log.pos); + }); + + const myColor = + (game.user1_id.equals(user._id) && game.black == 1) || (game.user2_id.equals(user._id) && game.black == 2) + ? true + : false; + + if (!o.canPut(myColor, pos)) return; + o.put(myColor, pos); + + let winner; + if (o.isEnded) { + if (o.winner === true) { + winner = game.black == 1 ? game.user1_id : game.user2_id; + } else if (o.winner === false) { + winner = game.black == 1 ? game.user2_id : game.user1_id; + } else { + winner = null; + } + } + + const log = { + at: new Date(), + color: myColor, + pos + }; + + const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); + + await Game.update({ + _id: gameId + }, { + $set: { + crc32, + is_ended: o.isEnded, + winner_id: winner + }, + $push: { + logs: log + } + }); + + publishOthelloGameStream(gameId, 'set', Object.assign(log, { + next: o.turn + })); + + if (o.isEnded) { + publishOthelloGameStream(gameId, 'ended', { + winner_id: winner, + game: await pack(gameId, user) + }); + } + } + + async function check(crc32) { + const game = await Game.findOne({ _id: gameId }); + + if (!game.is_started) return; + + // 互換性のため + if (game.crc32 == null) return; + + if (crc32 !== game.crc32) { + connection.send(JSON.stringify({ + type: 'rescue', + body: await pack(game, user) + })); + } + } +} diff --git a/src/server/api/stream/othello.ts b/src/server/api/stream/othello.ts new file mode 100644 index 0000000000..bd3b4a7637 --- /dev/null +++ b/src/server/api/stream/othello.ts @@ -0,0 +1,29 @@ +import * as mongo from 'mongodb'; +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import Matching, { pack } from '../models/othello-matching'; +import publishUserStream from '../event'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { + // Subscribe othello stream + subscriber.subscribe(`misskey:othello-stream:${user._id}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); + + connection.on('message', async (data) => { + const msg = JSON.parse(data.utf8Data); + + switch (msg.type) { + case 'ping': + if (msg.id == null) return; + const matching = await Matching.findOne({ + parent_id: user._id, + child_id: new mongo.ObjectID(msg.id) + }); + if (matching == null) return; + publishUserStream(matching.child_id, 'othello_invited', await pack(matching, matching.child_id)); + break; + } + }); +} diff --git a/src/server/api/stream/requests.ts b/src/server/api/stream/requests.ts new file mode 100644 index 0000000000..d7bb5e6c5c --- /dev/null +++ b/src/server/api/stream/requests.ts @@ -0,0 +1,19 @@ +import * as websocket from 'websocket'; +import Xev from 'xev'; + +const ev = new Xev(); + +export default function(request: websocket.request, connection: websocket.connection): void { + const onRequest = request => { + connection.send(JSON.stringify({ + type: 'request', + body: request + })); + }; + + ev.addListener('request', onRequest); + + connection.on('close', () => { + ev.removeListener('request', onRequest); + }); +} diff --git a/src/server/api/stream/server.ts b/src/server/api/stream/server.ts new file mode 100644 index 0000000000..4ca2ad1b10 --- /dev/null +++ b/src/server/api/stream/server.ts @@ -0,0 +1,19 @@ +import * as websocket from 'websocket'; +import Xev from 'xev'; + +const ev = new Xev(); + +export default function(request: websocket.request, connection: websocket.connection): void { + const onStats = stats => { + connection.send(JSON.stringify({ + type: 'stats', + body: stats + })); + }; + + ev.addListener('stats', onStats); + + connection.on('close', () => { + ev.removeListener('stats', onStats); + }); +} diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts new file mode 100644 index 0000000000..95f444e00b --- /dev/null +++ b/src/server/api/streaming.ts @@ -0,0 +1,118 @@ +import * as http from 'http'; +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import config from '../../conf'; +import { default as User, IUser } from './models/user'; +import AccessToken from './models/access-token'; +import isNativeToken from './common/is-native-token'; + +import homeStream from './stream/home'; +import driveStream from './stream/drive'; +import messagingStream from './stream/messaging'; +import messagingIndexStream from './stream/messaging-index'; +import othelloGameStream from './stream/othello-game'; +import othelloStream from './stream/othello'; +import serverStream from './stream/server'; +import requestsStream from './stream/requests'; +import channelStream from './stream/channel'; + +module.exports = (server: http.Server) => { + /** + * Init websocket server + */ + const ws = new websocket.server({ + httpServer: server + }); + + ws.on('request', async (request) => { + const connection = request.accept(); + + if (request.resourceURL.pathname === '/server') { + serverStream(request, connection); + return; + } + + if (request.resourceURL.pathname === '/requests') { + requestsStream(request, connection); + return; + } + + // Connect to Redis + const subscriber = redis.createClient( + config.redis.port, config.redis.host); + + connection.on('close', () => { + subscriber.unsubscribe(); + subscriber.quit(); + }); + + if (request.resourceURL.pathname === '/channel') { + channelStream(request, connection, subscriber); + return; + } + + const user = await authenticate(request.resourceURL.query.i); + + if (request.resourceURL.pathname === '/othello-game') { + othelloGameStream(request, connection, subscriber, user); + return; + } + + if (user == null) { + connection.send('authentication-failed'); + connection.close(); + return; + } + + const channel = + request.resourceURL.pathname === '/' ? homeStream : + request.resourceURL.pathname === '/drive' ? driveStream : + request.resourceURL.pathname === '/messaging' ? messagingStream : + request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream : + request.resourceURL.pathname === '/othello' ? othelloStream : + null; + + if (channel !== null) { + channel(request, connection, subscriber, user); + } else { + connection.close(); + } + }); +}; + +/** + * 接続してきたユーザーを取得します + * @param token 送信されてきたトークン + */ +function authenticate(token: string): Promise { + if (token == null) { + return Promise.resolve(null); + } + + return new Promise(async (resolve, reject) => { + if (isNativeToken(token)) { + // Fetch user + const user: IUser = await User + .findOne({ + host: null, + 'account.token': token + }); + + resolve(user); + } else { + const accessToken = await AccessToken.findOne({ + hash: token + }); + + if (accessToken == null) { + return reject('invalid signature'); + } + + // Fetch user + const user: IUser = await User + .findOne({ _id: accessToken.user_id }); + + resolve(user); + } + }); +} -- cgit v1.2.3-freya From bfc193d8cd9aecdb82d585e8b4e101deac60a5bb Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 29 Mar 2018 14:48:47 +0900 Subject: Resolve conflicts --- .config/example.yml | 57 ++ .gitignore | 3 +- CHANGELOG.md | 532 ---------------- LICENSE | 682 ++++++++++++++++++++- LICENSE_AGPL-3.0 | 661 -------------------- README.md | 53 +- docker/Dockerfile | 2 +- docs/config.md | 60 -- docs/setup.en.md | 6 +- docs/setup.ja.md | 6 +- gulpfile.ts | 1 - locales/ja.yml | 2 +- src/index.ts | 1 - src/server/api/authenticate.ts | 4 +- src/server/api/bot/core.ts | 8 +- src/server/api/bot/interfaces/line.ts | 12 +- src/server/api/common/drive/add-file.ts | 16 +- src/server/api/common/get-friends.ts | 8 +- src/server/api/common/notify.ts | 18 +- src/server/api/common/push-sw.ts | 4 +- src/server/api/common/read-messaging-message.ts | 12 +- src/server/api/common/read-notification.ts | 8 +- src/server/api/common/watch-post.ts | 14 +- src/server/api/endpoints.ts | 2 +- src/server/api/endpoints/aggregation/posts.ts | 16 +- .../api/endpoints/aggregation/posts/reaction.ts | 16 +- .../api/endpoints/aggregation/posts/reactions.ts | 16 +- .../api/endpoints/aggregation/posts/reply.ts | 14 +- .../api/endpoints/aggregation/posts/repost.ts | 16 +- src/server/api/endpoints/aggregation/users.ts | 8 +- .../api/endpoints/aggregation/users/activity.ts | 24 +- .../api/endpoints/aggregation/users/followers.ts | 20 +- .../api/endpoints/aggregation/users/following.ts | 20 +- src/server/api/endpoints/aggregation/users/post.ts | 24 +- .../api/endpoints/aggregation/users/reaction.ts | 16 +- src/server/api/endpoints/app/create.ts | 26 +- src/server/api/endpoints/app/name_id/available.ts | 18 +- src/server/api/endpoints/app/show.ts | 24 +- src/server/api/endpoints/auth/accept.ts | 14 +- src/server/api/endpoints/auth/session/generate.ts | 12 +- src/server/api/endpoints/auth/session/show.ts | 6 +- src/server/api/endpoints/auth/session/userkey.ts | 20 +- src/server/api/endpoints/channels.ts | 16 +- src/server/api/endpoints/channels/create.ts | 12 +- src/server/api/endpoints/channels/posts.ts | 24 +- src/server/api/endpoints/channels/show.ts | 6 +- src/server/api/endpoints/channels/unwatch.ts | 16 +- src/server/api/endpoints/channels/watch.ts | 20 +- src/server/api/endpoints/drive.ts | 4 +- src/server/api/endpoints/drive/files.ts | 26 +- src/server/api/endpoints/drive/files/create.ts | 6 +- src/server/api/endpoints/drive/files/find.ts | 10 +- src/server/api/endpoints/drive/files/show.ts | 8 +- src/server/api/endpoints/drive/files/update.ts | 22 +- .../api/endpoints/drive/files/upload_from_url.ts | 6 +- src/server/api/endpoints/drive/folders.ts | 26 +- src/server/api/endpoints/drive/folders/create.ts | 14 +- src/server/api/endpoints/drive/folders/find.ts | 10 +- src/server/api/endpoints/drive/folders/show.ts | 8 +- src/server/api/endpoints/drive/folders/update.ts | 32 +- src/server/api/endpoints/drive/stream.ts | 18 +- src/server/api/endpoints/following/create.ts | 22 +- src/server/api/endpoints/following/delete.ts | 18 +- src/server/api/endpoints/i.ts | 2 +- src/server/api/endpoints/i/2fa/done.ts | 8 +- src/server/api/endpoints/i/2fa/register.ts | 2 +- src/server/api/endpoints/i/2fa/unregister.ts | 4 +- src/server/api/endpoints/i/appdata/get.ts | 4 +- src/server/api/endpoints/i/appdata/set.ts | 8 +- src/server/api/endpoints/i/authorized_apps.ts | 4 +- src/server/api/endpoints/i/change_password.ts | 12 +- src/server/api/endpoints/i/favorites.ts | 4 +- src/server/api/endpoints/i/notifications.ts | 34 +- src/server/api/endpoints/i/pin.ts | 10 +- src/server/api/endpoints/i/signin_history.ts | 18 +- src/server/api/endpoints/i/update.ts | 38 +- .../api/endpoints/i/update_client_setting.ts | 4 +- src/server/api/endpoints/i/update_home.ts | 6 +- src/server/api/endpoints/i/update_mobile_home.ts | 6 +- src/server/api/endpoints/messaging/history.ts | 14 +- src/server/api/endpoints/messaging/messages.ts | 36 +- .../api/endpoints/messaging/messages/create.ts | 74 +-- src/server/api/endpoints/messaging/unread.ts | 12 +- src/server/api/endpoints/meta.ts | 1 - src/server/api/endpoints/mute/create.ts | 18 +- src/server/api/endpoints/mute/delete.ts | 14 +- src/server/api/endpoints/mute/list.ts | 8 +- src/server/api/endpoints/my/apps.ts | 2 +- .../endpoints/notifications/get_unread_count.ts | 12 +- .../endpoints/notifications/mark_as_read_all.ts | 6 +- src/server/api/endpoints/othello/games.ts | 28 +- src/server/api/endpoints/othello/games/show.ts | 16 +- src/server/api/endpoints/othello/invitations.ts | 2 +- src/server/api/endpoints/othello/match.ts | 44 +- src/server/api/endpoints/othello/match/cancel.ts | 2 +- src/server/api/endpoints/posts.ts | 24 +- src/server/api/endpoints/posts/categorize.ts | 8 +- src/server/api/endpoints/posts/context.ts | 14 +- src/server/api/endpoints/posts/create.ts | 166 ++--- src/server/api/endpoints/posts/favorites/create.ts | 16 +- src/server/api/endpoints/posts/favorites/delete.ts | 12 +- src/server/api/endpoints/posts/mentions.ts | 18 +- .../api/endpoints/posts/polls/recommendation.ts | 8 +- src/server/api/endpoints/posts/polls/vote.ts | 34 +- src/server/api/endpoints/posts/reactions.ts | 10 +- src/server/api/endpoints/posts/reactions/create.ts | 46 +- src/server/api/endpoints/posts/reactions/delete.ts | 16 +- src/server/api/endpoints/posts/replies.ts | 8 +- src/server/api/endpoints/posts/reposts.ts | 24 +- src/server/api/endpoints/posts/search.ts | 98 +-- src/server/api/endpoints/posts/show.ts | 6 +- src/server/api/endpoints/posts/timeline.ts | 58 +- src/server/api/endpoints/posts/trend.ts | 12 +- src/server/api/endpoints/stats.ts | 8 +- src/server/api/endpoints/sw/register.ts | 6 +- src/server/api/endpoints/username/available.ts | 2 +- src/server/api/endpoints/users.ts | 4 +- src/server/api/endpoints/users/followers.ts | 14 +- src/server/api/endpoints/users/following.ts | 14 +- .../users/get_frequently_replied_users.ts | 20 +- src/server/api/endpoints/users/posts.ts | 62 +- src/server/api/endpoints/users/recommendation.ts | 4 +- src/server/api/endpoints/users/search.ts | 2 +- .../api/endpoints/users/search_by_username.ts | 2 +- src/server/api/endpoints/users/show.ts | 56 +- src/server/api/models/access-token.ts | 18 +- src/server/api/models/app.ts | 24 +- src/server/api/models/appdata.ts | 3 - src/server/api/models/auth-session.ts | 9 +- src/server/api/models/channel-watching.ts | 12 +- src/server/api/models/channel.ts | 15 +- src/server/api/models/drive-file.ts | 14 +- src/server/api/models/drive-folder.ts | 18 +- src/server/api/models/drive-tag.ts | 3 - src/server/api/models/favorite.ts | 11 +- src/server/api/models/following.ts | 12 +- src/server/api/models/messaging-history.ts | 12 +- src/server/api/models/messaging-message.ts | 19 +- src/server/api/models/meta.ts | 5 +- src/server/api/models/mute.ts | 12 +- src/server/api/models/notification.ts | 22 +- src/server/api/models/othello-game.ts | 42 +- src/server/api/models/othello-matching.ts | 12 +- src/server/api/models/poll-vote.ts | 16 +- src/server/api/models/post-reaction.ts | 10 +- src/server/api/models/post-watching.ts | 11 +- src/server/api/models/post.ts | 62 +- src/server/api/models/signin.ts | 5 + src/server/api/models/sw-subscription.ts | 12 +- src/server/api/models/user.ts | 133 ++-- src/server/api/private/signin.ts | 10 +- src/server/api/private/signup.ts | 26 +- src/server/api/service/github.ts | 2 +- src/server/api/service/twitter.ts | 10 +- src/server/api/stream/home.ts | 16 +- src/server/api/stream/othello-game.ts | 128 ++-- src/server/api/stream/othello.ts | 6 +- src/server/api/streaming.ts | 2 +- src/server/common/get-post-summary.ts | 4 +- src/server/common/othello/ai/back.ts | 30 +- src/server/common/othello/ai/front.ts | 16 +- src/server/common/user/get-summary.ts | 2 +- src/server/web/app/auth/views/form.vue | 2 +- src/server/web/app/auth/views/index.vue | 12 +- src/server/web/app/ch/tags/channel.tag | 28 +- src/server/web/app/common/define-widget.ts | 4 +- src/server/web/app/common/mios.ts | 4 +- .../web/app/common/scripts/compose-notification.ts | 12 +- .../web/app/common/scripts/parse-search-query.ts | 4 +- .../web/app/common/scripts/streaming/home.ts | 2 +- .../app/common/views/components/autocomplete.vue | 2 +- .../views/components/messaging-room.form.vue | 6 +- .../views/components/messaging-room.message.vue | 12 +- .../app/common/views/components/messaging-room.vue | 16 +- .../web/app/common/views/components/messaging.vue | 16 +- .../app/common/views/components/othello.game.vue | 58 +- .../common/views/components/othello.gameroom.vue | 2 +- .../app/common/views/components/othello.room.vue | 26 +- .../web/app/common/views/components/othello.vue | 22 +- .../web/app/common/views/components/poll.vue | 10 +- .../web/app/common/views/components/post-menu.vue | 4 +- .../common/views/components/reaction-picker.vue | 2 +- .../common/views/components/reactions-viewer.vue | 2 +- .../web/app/common/views/components/signin.vue | 6 +- .../web/app/common/views/components/signup.vue | 2 +- .../common/views/components/twitter-setting.vue | 4 +- .../web/app/common/views/components/uploader.vue | 2 +- .../common/views/components/welcome-timeline.vue | 4 +- .../app/common/views/directives/autocomplete.ts | 2 +- .../web/app/common/views/widgets/slideshow.vue | 2 +- .../web/app/common/views/widgets/version.vue | 7 +- src/server/web/app/config.ts | 4 +- src/server/web/app/desktop/api/update-avatar.ts | 8 +- src/server/web/app/desktop/api/update-banner.ts | 8 +- .../web/app/desktop/views/components/activity.vue | 2 +- .../app/desktop/views/components/drive.file.vue | 14 +- .../app/desktop/views/components/drive.folder.vue | 10 +- .../desktop/views/components/drive.nav-folder.vue | 8 +- .../web/app/desktop/views/components/drive.vue | 28 +- .../app/desktop/views/components/follow-button.vue | 22 +- .../desktop/views/components/followers-window.vue | 2 +- .../web/app/desktop/views/components/followers.vue | 6 +- .../desktop/views/components/following-window.vue | 2 +- .../web/app/desktop/views/components/following.vue | 6 +- .../app/desktop/views/components/friends-maker.vue | 2 +- .../web/app/desktop/views/components/home.vue | 20 +- .../app/desktop/views/components/media-image.vue | 2 +- .../web/app/desktop/views/components/mentions.vue | 2 +- .../app/desktop/views/components/notifications.vue | 38 +- .../desktop/views/components/post-detail.sub.vue | 8 +- .../app/desktop/views/components/post-detail.vue | 32 +- .../web/app/desktop/views/components/post-form.vue | 8 +- .../app/desktop/views/components/post-preview.vue | 8 +- .../desktop/views/components/posts.post.sub.vue | 8 +- .../app/desktop/views/components/posts.post.vue | 36 +- .../web/app/desktop/views/components/posts.vue | 4 +- .../app/desktop/views/components/repost-form.vue | 2 +- .../app/desktop/views/components/settings.2fa.vue | 8 +- .../desktop/views/components/settings.password.vue | 4 +- .../desktop/views/components/settings.profile.vue | 6 +- .../desktop/views/components/settings.signins.vue | 2 +- .../web/app/desktop/views/components/settings.vue | 14 +- .../desktop/views/components/sub-post-content.vue | 4 +- .../web/app/desktop/views/components/timeline.vue | 8 +- .../desktop/views/components/ui.header.account.vue | 2 +- .../web/app/desktop/views/components/ui.header.vue | 4 +- .../app/desktop/views/components/user-preview.vue | 12 +- .../desktop/views/components/users-list.item.vue | 4 +- .../desktop/views/components/widget-container.vue | 4 +- .../web/app/desktop/views/components/window.vue | 4 +- src/server/web/app/desktop/views/pages/home.vue | 2 +- src/server/web/app/desktop/views/pages/othello.vue | 2 +- src/server/web/app/desktop/views/pages/post.vue | 2 +- .../views/pages/user/user.followers-you-know.vue | 4 +- .../app/desktop/views/pages/user/user.friends.vue | 4 +- .../app/desktop/views/pages/user/user.header.vue | 10 +- .../web/app/desktop/views/pages/user/user.home.vue | 6 +- .../app/desktop/views/pages/user/user.photos.vue | 4 +- .../app/desktop/views/pages/user/user.profile.vue | 22 +- .../app/desktop/views/pages/user/user.timeline.vue | 8 +- src/server/web/app/desktop/views/pages/welcome.vue | 2 +- .../desktop/views/widgets/channel.channel.form.vue | 4 +- .../app/desktop/views/widgets/channel.channel.vue | 2 +- .../web/app/desktop/views/widgets/channel.vue | 2 +- .../web/app/desktop/views/widgets/profile.vue | 4 +- src/server/web/app/desktop/views/widgets/users.vue | 2 +- src/server/web/app/dev/views/app.vue | 2 +- src/server/web/app/dev/views/new-app.vue | 12 +- src/server/web/app/init.ts | 4 +- src/server/web/app/mobile/api/post.ts | 2 +- .../web/app/mobile/views/components/activity.vue | 2 +- .../mobile/views/components/drive.file-detail.vue | 14 +- .../web/app/mobile/views/components/drive.file.vue | 4 +- .../web/app/mobile/views/components/drive.vue | 38 +- .../app/mobile/views/components/follow-button.vue | 22 +- .../app/mobile/views/components/media-image.vue | 2 +- .../views/components/notification-preview.vue | 14 +- .../app/mobile/views/components/notification.vue | 16 +- .../app/mobile/views/components/notifications.vue | 6 +- .../web/app/mobile/views/components/post-card.vue | 2 +- .../mobile/views/components/post-detail.sub.vue | 4 +- .../app/mobile/views/components/post-detail.vue | 28 +- .../web/app/mobile/views/components/post-form.vue | 8 +- .../app/mobile/views/components/post-preview.vue | 4 +- .../web/app/mobile/views/components/post.sub.vue | 4 +- .../web/app/mobile/views/components/post.vue | 30 +- .../web/app/mobile/views/components/posts.vue | 4 +- .../mobile/views/components/sub-post-content.vue | 4 +- .../web/app/mobile/views/components/timeline.vue | 6 +- .../web/app/mobile/views/components/ui.header.vue | 4 +- .../web/app/mobile/views/components/ui.nav.vue | 2 +- .../web/app/mobile/views/components/user-card.vue | 4 +- .../app/mobile/views/components/user-preview.vue | 2 +- .../app/mobile/views/components/user-timeline.vue | 10 +- .../web/app/mobile/views/pages/followers.vue | 8 +- .../web/app/mobile/views/pages/following.vue | 8 +- src/server/web/app/mobile/views/pages/home.vue | 22 +- .../web/app/mobile/views/pages/notifications.vue | 2 +- src/server/web/app/mobile/views/pages/othello.vue | 2 +- src/server/web/app/mobile/views/pages/post.vue | 2 +- .../web/app/mobile/views/pages/profile-setting.vue | 8 +- src/server/web/app/mobile/views/pages/settings.vue | 7 +- src/server/web/app/mobile/views/pages/user.vue | 12 +- .../views/pages/user/home.followers-you-know.vue | 4 +- .../app/mobile/views/pages/user/home.friends.vue | 2 +- .../app/mobile/views/pages/user/home.photos.vue | 4 +- .../web/app/mobile/views/pages/user/home.posts.vue | 2 +- .../web/app/mobile/views/pages/user/home.vue | 4 +- src/server/web/app/mobile/views/pages/welcome.vue | 8 +- .../web/app/mobile/views/widgets/profile.vue | 4 +- src/server/web/app/stats/tags/index.tag | 4 +- src/server/web/docs/api.ja.pug | 4 +- .../web/docs/api/endpoints/posts/create.yaml | 8 +- .../web/docs/api/endpoints/posts/timeline.yaml | 8 +- src/server/web/docs/api/entities/drive-file.yaml | 6 +- src/server/web/docs/api/entities/post.yaml | 18 +- src/server/web/docs/api/entities/user.yaml | 36 +- src/tools/analysis/extract-user-domains.ts | 2 +- src/tools/analysis/extract-user-keywords.ts | 2 +- src/tools/analysis/predict-user-interst.ts | 2 +- swagger.js | 10 +- tools/migration/nighthike/1.js | 39 ++ tools/migration/nighthike/2.js | 39 ++ tools/migration/nighthike/3.js | 73 +++ tools/migration/nighthike/4.js | 232 +++++++ .../node.1522066477.user-account-keypair.js | 39 -- tools/migration/shell.1522038492.user-account.js | 41 -- tools/migration/shell.1522116709.user-host.js | 1 - .../migration/shell.1522116710.user-host_lower.js | 1 - webpack.config.ts | 3 +- 310 files changed, 3141 insertions(+), 3296 deletions(-) create mode 100644 .config/example.yml delete mode 100644 CHANGELOG.md delete mode 100644 LICENSE_AGPL-3.0 delete mode 100644 docs/config.md delete mode 100644 src/server/api/models/appdata.ts delete mode 100644 src/server/api/models/drive-tag.ts create mode 100644 tools/migration/nighthike/1.js create mode 100644 tools/migration/nighthike/2.js create mode 100644 tools/migration/nighthike/3.js create mode 100644 tools/migration/nighthike/4.js delete mode 100644 tools/migration/node.1522066477.user-account-keypair.js delete mode 100644 tools/migration/shell.1522038492.user-account.js delete mode 100644 tools/migration/shell.1522116709.user-host.js delete mode 100644 tools/migration/shell.1522116710.user-host_lower.js (limited to 'src/server/api') diff --git a/.config/example.yml b/.config/example.yml new file mode 100644 index 0000000000..0e167ccb77 --- /dev/null +++ b/.config/example.yml @@ -0,0 +1,57 @@ +# サーバーのメンテナ情報 +maintainer: + # メンテナの名前 + name: + + # メンテナの連絡先(URLかmailto形式のURL) + url: + +# (Misskeyを動かす)URL +url: + +# 待受ポート +port: + +# TLSの設定(利用しない場合は省略可能) +https: + # 証明書のパス... + key: + cert: + +# MongoDBの設定 +mongodb: + host: localhost + port: 27017 + db: misskey + user: + pass: + +# Redisの設定 +redis: + host: localhost + port: 6379 + pass: + +# reCAPTCHAの設定 +recaptcha: + site_key: + secret_key: + +# ServiceWrokerの設定 +sw: + # VAPIDの公開鍵 + public_key: + + # VAPIDの秘密鍵 + private_key: + +# Google Maps API +google_maps_api_key: + +# Twitterインテグレーションの設定(利用しない場合は省略可能) +twitter: + # インテグレーション用アプリのコンシューマーキー + consumer_key: + + # インテグレーション用アプリのコンシューマーシークレット + consumer_secret: diff --git a/.gitignore b/.gitignore index d0ae0b8085..be8689e2ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -/.config +/.config/* +!/.config/example.yml /.vscode /node_modules /build diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 6e69a7319e..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,532 +0,0 @@ -ChangeLog (Release Notes) -========================= -主に notable な changes を書いていきます - -3493 (2018/01/01) ------------------ -* なんか - -3460 (2017/12/23) ------------------ -* 検索で複数のユーザーを指定できるように -* 検索でユーザーを除外できるように -* など - -3451 (2017/12/22) ------------------ -* ミュート機能 - -3430 (2017/12/21) ------------------ -* oops - -3428 (2017/12/21) ------------------ -* バグ修正 - -3426 (2017/12/21) ------------------ -* 検索にpoll追加 - -3424 (2017/12/21) ------------------ -* 検索にrepost追加 -* など - -3422 (2017/12/21) ------------------ -* 検索にfollow追加 #1023 - -3420 (2017/12/21) ------------------ -* 検索機能を大幅に強化 - -3415 (2017/12/19) ------------------ -* デザインの調整 - -3404 (2017/12/17) ------------------ -* なんか - -3400 (2017/12/17) ------------------ -* なんか - -3392 (2017/12/17) ------------------ -* ドキュメントなど - -3390 (2017/12/16) ------------------ -* ドキュメントなど - -3347 (2017/12/11) ------------------ -* バグ修正 - -3342 (2017/12/11) ------------------ -* なんか - -3339 (2017/12/11) ------------------ -* なんか - -3334 (2017/12/10) ------------------ -* いい感じにした - -3322 (2017/12/10) ------------------ -* :art: - -3320 (2017/12/10) ------------------ -* なんか - -3310 (2017/12/09) ------------------ -* i18nなど - -3308 (2017/12/09) ------------------ -* :art: - -3294 (2017/12/09) ------------------ -* バグ修正 - -3292 (2017/12/09) ------------------ -* ユーザビリティの向上 - -3281 (2017/12/08) ------------------ -* 二段階認証の実装 (#967) - -3278 (2017/12/08) ------------------ -* :v: - -3272 (2017/12/08) ------------------ -* Fix bug - -3268 (2017/12/08) ------------------ -* :v: - -3263 (2017/12/08) ------------------ -* FontAwesome5に移行 - -3230 (2017/11/28) ------------------ -* :v: - -3219 (2017/11/28) ------------------ -* なんか - -3212 (2017/11/27) ------------------ -* なんか - -3201 (2017/11/23) ------------------ -* Twitterログインを実装 (#939) - -3196 (2017/11/23) ------------------ -* バグ修正 - -3194 (2017/11/23) ------------------ -* バグ修正 - -3191 (2017/11/23) ------------------ -* :v: - -3188 (2017/11/22) ------------------ -* バグ修正 - -3180 (2017/11/21) ------------------ -* バグ修正 - -3177 (2017/11/21) ------------------ -* ServiceWorker support - * Misskeyを開いていないときでも通知を受け取れるように(Chromeのみ) - -3165 (2017/11/20) ------------------ -* デスクトップ版でも通知バッジを表示 (#918) -* デザインの調整 -* バグ修正 - -3155 (2017/11/20) ------------------ -* デスクトップ版でユーザーの投稿グラフを見れるように - -3142 (2017/11/18) ------------------ -* バグ修正 - -3140 (2017/11/18) ------------------ -* ウィジェットをスクロールに追従させるように - -3136 (2017/11/17) ------------------ -* バグ修正 -* 通信の最適化 - -3131 (2017/11/17) ------------------ -* バグ修正 -* 通信の最適化 - -3124 (2017/11/16) ------------------ -* バグ修正 - -3121 (2017/11/16) ------------------ -* ブロードキャストウィジェットの強化 -* デザインのグリッチの修正 -* 通信の最適化 - -3113 (2017/11/15) ------------------ -* アクティビティのレンダリングの問題の修正など - -3110 (2017/11/15) ------------------ -* デザインの調整など - -3107 (2017/11/14) ------------------ -* デザインの調整 - -3104 (2017/11/14) ------------------ -* デスクトップ版ユーザーページのデザインの改良 -* バグ修正 - -3099 (2017/11/14) ------------------ -* デスクトップ版ユーザーページの強化 -* バグ修正 - -3093 (2017/11/14) ------------------ -* やった - -3089 (2017/11/14) ------------------ -* なんか - -3069 (2017/11/14) ------------------ -* ドライブウィンドウもポップアウトできるように -* デザインの調整 - -3066 (2017/11/14) ------------------ -* メッセージウィジェット追加 -* アクセスログウィジェット追加 - -3057 (2017/11/13) ------------------ -* グリッチ修正 - -3055 (2017/11/13) ------------------ -* メッセージのウィンドウのポップアウト (#911) - -3050 (2017/11/13) ------------------ -* 通信の最適化 - * これで例えばサーバー情報ウィジェットを5000兆個設置しても利用するコネクションは一つだけになりウィジェットを1つ設置したときと(ネットワーク的な)負荷は変わらなくなる -* デザインの調整 -* ユーザビリティの向上 - -3040 (2017/11/12) ------------------ -* バグ修正 - -3038 (2017/11/12) ------------------ -* 投稿フォームウィジェットの追加 -* タイムライン上部にもウィジェットを配置できるように - -3035 (2017/11/12) ------------------ -* ウィジェットの強化 - -3033 (2017/11/12) ------------------ -* デザインの調整 - -3031 (2017/11/12) ------------------ -* ウィジェットの強化 - -3028 (2017/11/12) ------------------ -* ウィジェットの表示をコンパクトにできるように - -3026 (2017/11/12) ------------------ -* バグ修正 - -3024 (2017/11/12) ------------------ -* いい感じにするなど - -3020 (2017/11/12) ------------------ -* 通信の最適化 - -3017 (2017/11/11) ------------------ -* 誤字修正など - -3012 (2017/11/11) ------------------ -* デザインの調整 - -3010 (2017/11/11) ------------------ -* デザインの調整 - -3008 (2017/11/11) ------------------ -* カレンダー(タイムマシン)ウィジェットの追加 - -3006 (2017/11/11) ------------------ -* デザインの調整 -* など - -2996 (2017/11/10) ------------------ -* デザインの調整 -* など - -2991 (2017/11/09) ------------------ -* デザインの調整 - -2988 (2017/11/09) ------------------ -* チャンネルウィジェットを追加 - -2984 (2017/11/09) ------------------ -* スライドショーウィジェットを追加 - -2974 (2017/11/08) ------------------ -* ホームのカスタマイズを実装するなど - -2971 (2017/11/08) ------------------ -* バグ修正 -* デザインの調整 -* i18n - -2944 (2017/11/07) ------------------ -* パフォーマンスの向上 - * GirdFSになるなどした -* 依存関係の更新 - -2807 (2017/11/02) ------------------ -* いい感じに - -2805 (2017/11/02) ------------------ -* いい感じに - -2801 (2017/11/01) ------------------ -* チャンネルのWatch実装 - -2799 (2017/11/01) ------------------ -* いい感じに - -2795 (2017/11/01) ------------------ -* いい感じに - -2793 (2017/11/01) ------------------ -* なんか - -2783 (2017/11/01) ------------------ -* なんか - -2777 (2017/11/01) ------------------ -* 細かいブラッシュアップ - -2775 (2017/11/01) ------------------ -* Fix: バグ修正 - -2769 (2017/11/01) ------------------ -* New: チャンネルシステム - -2752 (2017/10/30) ------------------ -* New: 未読の通知がある場合アイコンを表示するように - -2747 (2017/10/25) ------------------ -* Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89) - -2742 (2017/10/25) ------------------ -* New: トラブルシューティングを実装するなど - -2735 (2017/10/22) ------------------ -* New: モバイル版からでもクライアントバージョンを確認できるように - -2732 (2017/10/22) ------------------ -* 依存関係の更新など - -2584 (2017/09/08) ------------------ -* New: ユーザーページによく使うドメインを表示 (#771) -* New: よくリプライするユーザーをユーザーページに表示 (#770) - -2566 (2017/09/07) ------------------ -* New: 投稿することの多いキーワードをユーザーページに表示する (#768) -* l10n -* デザインの修正 - -2544 (2017/09/06) ------------------ -* 投稿のカテゴリに関する実験的な実装 -* l10n -* ユーザビリティの向上 - -2520 (2017/08/30) ------------------ -* デザインの調整 - -2518 (2017/08/30) ------------------ -* Fix: モバイル版のタイムラインからリアクションやメニューを開けない -* デザインの調整 - -2515 (2017/08/30) ------------------ -* New: 投稿のピン留め (#746) -* New: モバイル版のユーザーページに知り合いのフォロワーを表示するように -* New: ホームストリームにメッセージを流すことでlast_used_atを更新できるようにする (#745) -* その他細かな修正 - -2508 (2017/08/30) ------------------ -* New: モバイル版のユーザーページのアクティビティチャートを変更 -* New: モバイル版のユーザーページに最終ログイン日時を表示するように -* デザインの調整 - -2503 (2017/08/30) ------------------ -* デザインの調整 - -2502 (2017/08/30) ------------------ -* デザインの修正・調整 - -2501 (2017/08/30) ------------------ -* New: モバイルのユーザーページを刷新 - -2498 (2017/08/29) ------------------ -* Fix: repostのborder-radiusが効いていない (#743) -* テーマカラーを赤に戻してみた -* ユーザビリティの向上 -* デザインの調整 - -2493-2 (2017/08/29) -------------------- -* デザインの修正 - -2493 (2017/08/29) ------------------ -* デザインの変更など - -2491 (2017/08/29) ------------------ -* デザインの修正と調整 - -2489 (2017/08/29) ------------------ -* ユーザビリティの向上 -* デザインの調整 - -2487 (2017/08/29) ------------------ -* New: パスワードを変更する際に新しいパスワードを二度入力させる (#739) -* New: ドナーを表示する (#738) -* Fix: 投稿のリンクが機能していない問題を修正 -* Fix: アカウント作成フォームのユーザーページURLプレビューが正しく機能していなかった問題を修正 -* l10n -* デザインの調整 - -2470 (2017/08/29) ------------------ -* New: トークンを再生成できるように (#497) -* New: パスワードを変更する機能 (#364) - -2461 (2017/08/28) ------------------ -* Fix: モバイル版からアバターとバナーの設定を行えなかった問題を修正 -* デザインの修正 - -2458 (2017/08/28) ------------------ -* New: モバイル版からプロフィールを設定できるように -* New: モバイル版からサインアウトを行えるように -* New: 投稿ページに次の投稿/前の投稿リンクを作成 (#734) -* New: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように -* Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736) -* Fix: モバイル版で設定にアクセスできない -* デザインの調整 -* 依存関係の更新 - -2380 ----- -アプリケーションが作れない問題を修正 - -2367 ----- -Statsのユーザー数グラフに「アカウントが作成された**回数**」(その日時点での「アカウント数」**ではなく**)グラフも併記するようにした - -2364 ----- -デザインの微調整 - -2361 ----- -Statsを実装するなど - -2357 ----- -Statusを実装するなど diff --git a/LICENSE b/LICENSE index 0b6e30e45f..dba13ed2dd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,661 @@ -The MIT License (MIT) - -Copyright (c) 2014-2018 syuilo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSE_AGPL-3.0 b/LICENSE_AGPL-3.0 deleted file mode 100644 index dba13ed2dd..0000000000 --- a/LICENSE_AGPL-3.0 +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/README.md b/README.md index c50566cc95..ef12958c2f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -![Misskey](./assets/title.png) + + +Misskey ================================================================ [![][travis-badge]][travis-link] @@ -7,57 +9,46 @@ [![][sakurako-badge]][himasaku] [![][agpl-3.0-badge]][AGPL-3.0] -[Misskey](https://misskey.xyz) is a completely open source, -ultimately sophisticated new type of mini-blog based SNS. +> Lead Maintainer: [syuilo][syuilo-link] -![ss](./assets/ss.jpg) +**[Misskey](https://misskey.xyz)** is a completely open source, +ultimately sophisticated new type of mini-blog based SNS. -Key features +:sparkles: Features ---------------------------------------------------------------- * Automatically updated timeline * Private messages * Two-Factor Authentication support * ServiceWorker support * Web API for third-party applications -* No ads +* ActivityPub compatible and more! You can touch with your own eyes at https://misskey.xyz/. -Setup and Installation +:package: Setup and Installation ---------------------------------------------------------------- If you want to run your own instance of Misskey, please see [Setup and installation guide](./docs/setup.en.md). -Contribution ----------------------------------------------------------------- -Please see [Contribution guide](./CONTRIBUTING.md). - -Release Notes ----------------------------------------------------------------- -Please see [ChangeLog](./CHANGELOG.md). - -Sponsors & Backers +:yen: Donation ---------------------------------------------------------------- -Misskey has no 100+ GitHub stars currently. However, a donation is always welcome! -If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link]. +If you want to donate to Misskey, please see [this](./docs/donate.ja.md). -**Note:** When you donate to Misskey, your name will be listed in [donors](./DONORS.md). +[List of all donors](./DONORS.md) -Collaborators +:mortar_board: Notable contributors ---------------------------------------------------------------- -| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | -|------------------------|-----------------------------------|---------------------------------| -| [syuilo][syuilo-link] | [Aya Morisawa][ayamorisawa-link] | [otofune][otofune-link] | +| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![rinsuki][rinsuki-icon] | +|:-:|:-:|:-:|:-:|:-:| +| [syuilo][syuilo-link]
Owner | [Aya Morisawa][ayamorisawa-link]
Collaborator | [otofune][otofune-link]
Collaborator | [akihikodaki][akihikodaki-link] | [rinsuki][rinsuki-link] | [List of all contributors](https://github.com/syuilo/misskey/graphs/contributors) -Copyright +:four_leaf_clover: Copyright ---------------------------------------------------------------- -Misskey is an open-source software licensed under [The MIT License](LICENSE). +> Copyright (c) 2014-2018 syuilo -The portions of Misskey contributed by Akihiko Odaki is -licensed under GNU Affero General Public License (only version 3.0 of the -license is applied.) See Git log to identify them. +Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE). [agpl-3.0]: https://www.gnu.org/licenses/agpl-3.0.en.html [agpl-3.0-badge]: https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square @@ -69,10 +60,14 @@ license is applied.) See Git log to identify them. [himawari-badge]: https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square [sakurako-badge]: https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square - + [syuilo-link]: https://syuilo.com [syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 [ayamorisawa-link]: https://github.com/ayamorisawa [ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70 [otofune-link]: https://github.com/otofune [otofune-icon]: https://avatars0.githubusercontent.com/u/15062473?v=3&s=70 +[akihikodaki-link]: https://github.com/akihikodaki +[akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4 +[rinsuki-link]: https://github.com/rinsuki +[rinsuki-icon]: https://avatars0.githubusercontent.com/u/6533808?s=70&v=4 diff --git a/docker/Dockerfile b/docker/Dockerfile index ef04fc9e26..7cee650de3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,7 +14,7 @@ RUN pacman -S --noconfirm pacman RUN pacman-db-upgrade RUN pacman -S --noconfirm archlinux-keyring RUN pacman -Syyu --noconfirm -RUN pacman -S --noconfirm git nodejs npm mongodb redis graphicsmagick +RUN pacman -S --noconfirm git nodejs npm mongodb redis imagemagick COPY misskey.sh /root/misskey.sh RUN chmod u+x /root/misskey.sh diff --git a/docs/config.md b/docs/config.md deleted file mode 100644 index c4a54c0bee..0000000000 --- a/docs/config.md +++ /dev/null @@ -1,60 +0,0 @@ -``` yaml -# サーバーのメンテナ情報 -maintainer: - # メンテナの名前 - name: - - # メンテナの連絡先(URLかmailto形式のURL) - url: - -# (Misskeyを動かす)URL -url: - -# 待受ポート -port: - -# TLSの設定(利用しない場合は省略可能) -https: - # 証明書のパス... - key: - cert: - -# MongoDBの設定 -mongodb: - host: localhost - port: 27017 - db: misskey - user: - pass: - -# Redisの設定 -redis: - host: localhost - port: 6379 - pass: - -# reCAPTCHAの設定 -recaptcha: - site_key: - secret_key: - -# ServiceWrokerの設定 -sw: - # VAPIDの公開鍵 - public_key: - - # VAPIDの秘密鍵 - private_key: - -# Google Maps API -google_maps_api_key: - -# Twitterインテグレーションの設定(利用しない場合は省略可能) -twitter: - # インテグレーション用アプリのコンシューマーキー - consumer_key: - - # インテグレーション用アプリのコンシューマーシークレット - consumer_secret: - -``` diff --git a/docs/setup.en.md b/docs/setup.en.md index 08cd16857d..a436d751c8 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -37,9 +37,9 @@ Please install and setup these softwares: *3.* Prepare configuration ---------------------------------------------------------------- -First, you need to create a `.config` directory in the directory that -Misskey installed. And then you need to create a `default.yml` file in -the directory. The template of configuration is available [here](./config.md). +1. Copy `example.yml` of `.config` directory +2. Rename it to `default.yml` +3. Edit it *4.* Install and build Misskey ---------------------------------------------------------------- diff --git a/docs/setup.ja.md b/docs/setup.ja.md index 9fa56acb22..6605461d9f 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -37,9 +37,9 @@ web-push generate-vapid-keys *3.* 設定ファイルを用意する ---------------------------------------------------------------- -Misskeyをインストールしたディレクトリに、`.config`というディレクトリを作成し、 -その中に`default.yml`という名前で設定ファイルを作ってください。 -設定ファイルの下書きは[ここ](./config.md)にありますので、コピペしてご利用ください。 +1. `.config`ディレクトリ内の`example.yml`をコピー +2. `default.yml`にリネーム +3. 編集する *4.* Misskeyのインストール(とビルド) ---------------------------------------------------------------- diff --git a/gulpfile.ts b/gulpfile.ts index 11f34c9629..46727126ce 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -125,7 +125,6 @@ gulp.task('build:client:script', () => .pipe(replace('VERSION', JSON.stringify(version))) .pipe(replace('API', JSON.stringify(config.api_url))) .pipe(replace('ENV', JSON.stringify(env))) - .pipe(replace('HOST', JSON.stringify(config.host))) .pipe(isProduction ? uglify({ toplevel: true } as any) : gutil.noop()) diff --git a/locales/ja.yml b/locales/ja.yml index f826b1b6ca..fd140ecc30 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -147,7 +147,7 @@ common: available: "利用できます" unavailable: "既に利用されています" error: "通信エラー" - invalid-format: "a~z、A~Z、0~9、-(ハイフン)が使えます" + invalid-format: "a~z、A~Z、0~9、_が使えます" too-short: "3文字以上でお願いします!" too-long: "20文字以内でお願いします" password: "パスワード" diff --git a/src/index.ts b/src/index.ts index bd9b094d98..f86c768fdc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,7 +109,6 @@ async function workerMain(opt) { */ async function init(): Promise { Logger.info('Welcome to Misskey!'); - Logger.info(chalk.bold('Misskey ')); Logger.info('Initializing...'); EnvironmentInfo.show(); diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts index 537c3d1e1f..7b3983a838 100644 --- a/src/server/api/authenticate.ts +++ b/src/server/api/authenticate.ts @@ -55,10 +55,10 @@ export default (req: express.Request) => new Promise(async (resolv } const app = await App - .findOne({ _id: accessToken.app_id }); + .findOne({ _id: accessToken.appId }); const user = await User - .findOne({ _id: accessToken.user_id }); + .findOne({ _id: accessToken.userId }); return resolve({ app: app, diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts index 77a68aaee6..ec7c935f9e 100644 --- a/src/server/api/bot/core.ts +++ b/src/server/api/bot/core.ts @@ -67,7 +67,7 @@ export default class BotCore extends EventEmitter { return await this.context.q(query); } - if (/^@[a-zA-Z0-9-]+$/.test(query)) { + if (/^@[a-zA-Z0-9_]+$/.test(query)) { return await this.showUserCommand(query); } @@ -208,7 +208,7 @@ class SigninContext extends Context { if (this.temporaryUser == null) { // Fetch user const user: IUser = await User.findOne({ - username_lower: query.toLowerCase(), + usernameLower: query.toLowerCase(), host: null }, { fields: { @@ -297,7 +297,7 @@ class TlContext extends Context { private async getTl() { const tl = await require('../endpoints/posts/timeline')({ limit: 5, - until_id: this.next ? this.next : undefined + untilId: this.next ? this.next : undefined }, this.bot.user); if (tl.length > 0) { @@ -349,7 +349,7 @@ class NotificationsContext extends Context { private async getNotifications() { const notifications = await require('../endpoints/i/notifications')({ limit: 5, - until_id: this.next ? this.next : undefined + untilId: this.next ? this.next : undefined }, this.bot.user); if (notifications.length > 0) { diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts index 5b3e9107f6..1340ac9923 100644 --- a/src/server/api/bot/interfaces/line.ts +++ b/src/server/api/bot/interfaces/line.ts @@ -115,7 +115,7 @@ class LineBot extends BotCore { actions.push({ type: 'uri', label: 'Twitterアカウントを見る', - uri: `https://twitter.com/${user.account.twitter.screen_name}` + uri: `https://twitter.com/${user.account.twitter.screenName}` }); } @@ -130,7 +130,7 @@ class LineBot extends BotCore { altText: await super.showUserCommand(q), template: { type: 'buttons', - thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`, + thumbnailImageUrl: `${user.avatarUrl}?thumbnail&size=1024`, title: `${user.name} (@${acct})`, text: user.description || '(no description)', actions: actions @@ -142,7 +142,7 @@ class LineBot extends BotCore { public async showUserTimelinePostback(userId: string) { const tl = await require('../../endpoints/users/posts')({ - user_id: userId, + userId: userId, limit: 5 }, this.user); @@ -174,7 +174,7 @@ module.exports = async (app: express.Application) => { const user = await User.findOne({ host: null, 'account.line': { - user_id: sourceId + userId: sourceId } }); @@ -184,7 +184,7 @@ module.exports = async (app: express.Application) => { User.update(user._id, { $set: { 'account.line': { - user_id: sourceId + userId: sourceId } } }); @@ -194,7 +194,7 @@ module.exports = async (app: express.Application) => { User.update(user._id, { $set: { 'account.line': { - user_id: null + userId: null } } }); diff --git a/src/server/api/common/drive/add-file.ts b/src/server/api/common/drive/add-file.ts index 5f3c69c15a..21ddd1aae3 100644 --- a/src/server/api/common/drive/add-file.ts +++ b/src/server/api/common/drive/add-file.ts @@ -100,7 +100,7 @@ const addFile = async ( // Check if there is a file with the same hash const much = await DriveFile.findOne({ md5: hash, - 'metadata.user_id': user._id + 'metadata.userId': user._id }); if (much !== null) { @@ -172,7 +172,7 @@ const addFile = async ( } const driveFolder = await DriveFolder.findOne({ _id: folderId, - user_id: user._id + userId: user._id }); if (!driveFolder) { throw 'folder-not-found'; @@ -184,7 +184,7 @@ const addFile = async ( // Calculate drive usage const usage = await DriveFile .aggregate([{ - $match: { 'metadata.user_id': user._id } + $match: { 'metadata.userId': user._id } }, { $project: { length: true @@ -205,7 +205,7 @@ const addFile = async ( log(`drive usage is ${usage}`); // If usage limit exceeded - if (usage + size > user.drive_capacity) { + if (usage + size > user.driveCapacity) { throw 'no-free-space'; } })() @@ -221,12 +221,12 @@ const addFile = async ( } if (averageColor) { - properties['average_color'] = averageColor; + properties['avgColor'] = averageColor; } return addToGridFS(detectedName, readable, mime, { - user_id: user._id, - folder_id: folder !== null ? folder._id : null, + userId: user._id, + folderId: folder !== null ? folder._id : null, comment: comment, properties: properties }); @@ -297,7 +297,7 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi id: file._id.toString(), body: { name: file.name, - user_id: user._id.toString() + userId: user._id.toString() } }); } diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts index db6313816d..7f548b3bbf 100644 --- a/src/server/api/common/get-friends.ts +++ b/src/server/api/common/get-friends.ts @@ -6,17 +6,17 @@ export default async (me: mongodb.ObjectID, includeMe: boolean = true) => { // SELECT followee const myfollowing = await Following .find({ - follower_id: me, + followerId: me, // 削除されたドキュメントは除く - deleted_at: { $exists: false } + deletedAt: { $exists: false } }, { fields: { - followee_id: true + followeeId: true } }); // ID list of other users who the I follows - const myfollowingIds = myfollowing.map(follow => follow.followee_id); + const myfollowingIds = myfollowing.map(follow => follow.followeeId); if (includeMe) { myfollowingIds.push(me); diff --git a/src/server/api/common/notify.ts b/src/server/api/common/notify.ts index ae5669b84c..c4df17f880 100644 --- a/src/server/api/common/notify.ts +++ b/src/server/api/common/notify.ts @@ -16,11 +16,11 @@ export default ( // Create notification const notification = await Notification.insert(Object.assign({ - created_at: new Date(), - notifiee_id: notifiee, - notifier_id: notifier, + createdAt: new Date(), + notifieeId: notifiee, + notifierId: notifier, type: type, - is_read: false + isRead: false }, content)); resolve(notification); @@ -31,14 +31,14 @@ export default ( // 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する setTimeout(async () => { - const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true }); - if (!fresh.is_read) { + const fresh = await Notification.findOne({ _id: notification._id }, { isRead: true }); + if (!fresh.isRead) { //#region ただしミュートしているユーザーからの通知なら無視 const mute = await Mute.find({ - muter_id: notifiee, - deleted_at: { $exists: false } + muterId: notifiee, + deletedAt: { $exists: false } }); - const mutedUserIds = mute.map(m => m.mutee_id.toString()); + const mutedUserIds = mute.map(m => m.muteeId.toString()); if (mutedUserIds.indexOf(notifier.toString()) != -1) { return; } diff --git a/src/server/api/common/push-sw.ts b/src/server/api/common/push-sw.ts index b33715eb18..e5fbec10ec 100644 --- a/src/server/api/common/push-sw.ts +++ b/src/server/api/common/push-sw.ts @@ -20,7 +20,7 @@ export default async function(userId: mongo.ObjectID | string, type, body?) { // Fetch const subscriptions = await Subscription.find({ - user_id: userId + userId: userId }); subscriptions.forEach(subscription => { @@ -41,7 +41,7 @@ export default async function(userId: mongo.ObjectID | string, type, body?) { if (err.statusCode == 410) { Subscription.remove({ - user_id: userId, + userId: userId, endpoint: subscription.endpoint, auth: subscription.auth, publickey: subscription.publickey diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts index 8e5e5b2b68..9047edec8c 100644 --- a/src/server/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -37,12 +37,12 @@ export default ( // Update documents await Message.update({ _id: { $in: ids }, - user_id: otherpartyId, - recipient_id: userId, - is_read: false + userId: otherpartyId, + recipientId: userId, + isRead: false }, { $set: { - is_read: true + isRead: true } }, { multi: true @@ -55,8 +55,8 @@ export default ( // Calc count of my unread messages const count = await Message .count({ - recipient_id: userId, - is_read: false + recipientId: userId, + isRead: false }); if (count == 0) { diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts index 3009cc5d08..5bbf136323 100644 --- a/src/server/api/common/read-notification.ts +++ b/src/server/api/common/read-notification.ts @@ -29,10 +29,10 @@ export default ( // Update documents await Notification.update({ _id: { $in: ids }, - is_read: false + isRead: false }, { $set: { - is_read: true + isRead: true } }, { multi: true @@ -41,8 +41,8 @@ export default ( // Calc count of my unread notifications const count = await Notification .count({ - notifiee_id: userId, - is_read: false + notifieeId: userId, + isRead: false }); if (count == 0) { diff --git a/src/server/api/common/watch-post.ts b/src/server/api/common/watch-post.ts index 1a50f0edaa..61ea444430 100644 --- a/src/server/api/common/watch-post.ts +++ b/src/server/api/common/watch-post.ts @@ -3,15 +3,15 @@ import Watching from '../models/post-watching'; export default async (me: mongodb.ObjectID, post: object) => { // 自分の投稿はwatchできない - if (me.equals((post as any).user_id)) { + if (me.equals((post as any).userId)) { return; } // if watching now const exist = await Watching.findOne({ - post_id: (post as any)._id, - user_id: me, - deleted_at: { $exists: false } + postId: (post as any)._id, + userId: me, + deletedAt: { $exists: false } }); if (exist !== null) { @@ -19,8 +19,8 @@ export default async (me: mongodb.ObjectID, post: object) => { } await Watching.insert({ - created_at: new Date(), - post_id: (post as any)._id, - user_id: me + createdAt: new Date(), + postId: (post as any)._id, + userId: me }); }; diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index c7100bd036..979d8ac295 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -289,7 +289,7 @@ const endpoints: Endpoint[] = [ kind: 'notification-write' }, { - name: 'notifications/mark_as_read_all', + name: 'notifications/markAsRead_all', withCredential: true, kind: 'notification-write' }, diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts index 9d8bccbdb2..67d2619640 100644 --- a/src/server/api/endpoints/aggregation/posts.ts +++ b/src/server/api/endpoints/aggregation/posts.ts @@ -18,23 +18,23 @@ module.exports = params => new Promise(async (res, rej) => { const datas = await Post .aggregate([ { $project: { - repost_id: '$repost_id', - reply_id: '$reply_id', - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + repostId: '$repostId', + replyId: '$replyId', + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } }, type: { $cond: { - if: { $ne: ['$repost_id', null] }, + if: { $ne: ['$repostId', null] }, then: 'repost', else: { $cond: { - if: { $ne: ['$reply_id', null] }, + if: { $ne: ['$replyId', null] }, then: 'reply', else: 'post' } diff --git a/src/server/api/endpoints/aggregation/posts/reaction.ts b/src/server/api/endpoints/aggregation/posts/reaction.ts index eb99b9d088..9f9a4f37ee 100644 --- a/src/server/api/endpoints/aggregation/posts/reaction.ts +++ b/src/server/api/endpoints/aggregation/posts/reaction.ts @@ -12,9 +12,9 @@ import Reaction from '../../../models/post-reaction'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Lookup post const post = await Post.findOne({ @@ -27,15 +27,15 @@ module.exports = (params) => new Promise(async (res, rej) => { const datas = await Reaction .aggregate([ - { $match: { post_id: post._id } }, + { $match: { postId: post._id } }, { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } } }}, { $group: { diff --git a/src/server/api/endpoints/aggregation/posts/reactions.ts b/src/server/api/endpoints/aggregation/posts/reactions.ts index 790b523be9..2dc989281c 100644 --- a/src/server/api/endpoints/aggregation/posts/reactions.ts +++ b/src/server/api/endpoints/aggregation/posts/reactions.ts @@ -12,9 +12,9 @@ import Reaction from '../../../models/post-reaction'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Lookup post const post = await Post.findOne({ @@ -29,10 +29,10 @@ module.exports = (params) => new Promise(async (res, rej) => { const reactions = await Reaction .find({ - post_id: post._id, + postId: post._id, $or: [ - { deleted_at: { $exists: false } }, - { deleted_at: { $gt: startTime } } + { deletedAt: { $exists: false } }, + { deletedAt: { $gt: startTime } } ] }, { sort: { @@ -40,7 +40,7 @@ module.exports = (params) => new Promise(async (res, rej) => { }, fields: { _id: false, - post_id: false + postId: false } }); @@ -55,7 +55,7 @@ module.exports = (params) => new Promise(async (res, rej) => { // day = day.getTime(); const count = reactions.filter(r => - r.created_at < day && (r.deleted_at == null || r.deleted_at > day) + r.createdAt < day && (r.deletedAt == null || r.deletedAt > day) ).length; graph.push({ diff --git a/src/server/api/endpoints/aggregation/posts/reply.ts b/src/server/api/endpoints/aggregation/posts/reply.ts index b114c34e1e..3b050582a9 100644 --- a/src/server/api/endpoints/aggregation/posts/reply.ts +++ b/src/server/api/endpoints/aggregation/posts/reply.ts @@ -11,9 +11,9 @@ import Post from '../../../models/post'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Lookup post const post = await Post.findOne({ @@ -28,13 +28,13 @@ module.exports = (params) => new Promise(async (res, rej) => { .aggregate([ { $match: { reply: post._id } }, { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } } }}, { $group: { diff --git a/src/server/api/endpoints/aggregation/posts/repost.ts b/src/server/api/endpoints/aggregation/posts/repost.ts index 217159caa7..d9f3e36a07 100644 --- a/src/server/api/endpoints/aggregation/posts/repost.ts +++ b/src/server/api/endpoints/aggregation/posts/repost.ts @@ -11,9 +11,9 @@ import Post from '../../../models/post'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Lookup post const post = await Post.findOne({ @@ -26,15 +26,15 @@ module.exports = (params) => new Promise(async (res, rej) => { const datas = await Post .aggregate([ - { $match: { repost_id: post._id } }, + { $match: { repostId: post._id } }, { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } } }}, { $group: { diff --git a/src/server/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts index e38ce92ff9..a4e91a2282 100644 --- a/src/server/api/endpoints/aggregation/users.ts +++ b/src/server/api/endpoints/aggregation/users.ts @@ -22,8 +22,8 @@ module.exports = params => new Promise(async (res, rej) => { }, fields: { _id: false, - created_at: true, - deleted_at: true + createdAt: true, + deletedAt: true } }); @@ -44,11 +44,11 @@ module.exports = params => new Promise(async (res, rej) => { // day = day.getTime(); const total = users.filter(u => - u.created_at < dayEnd && (u.deleted_at == null || u.deleted_at > dayEnd) + u.createdAt < dayEnd && (u.deletedAt == null || u.deletedAt > dayEnd) ).length; const created = users.filter(u => - u.created_at < dayEnd && u.created_at > dayStart + u.createdAt < dayEnd && u.createdAt > dayStart ).length; graph.push({ diff --git a/src/server/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts index 102a71d7cb..d477616578 100644 --- a/src/server/api/endpoints/aggregation/users/activity.ts +++ b/src/server/api/endpoints/aggregation/users/activity.ts @@ -18,9 +18,9 @@ module.exports = (params) => new Promise(async (res, rej) => { const [limit = 365, limitErr] = $(params.limit).optional.number().range(1, 365).$; if (limitErr) return rej('invalid limit param'); - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Lookup user const user = await User.findOne({ @@ -37,25 +37,25 @@ module.exports = (params) => new Promise(async (res, rej) => { const datas = await Post .aggregate([ - { $match: { user_id: user._id } }, + { $match: { userId: user._id } }, { $project: { - repost_id: '$repost_id', - reply_id: '$reply_id', - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + repostId: '$repostId', + replyId: '$replyId', + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } }, type: { $cond: { - if: { $ne: ['$repost_id', null] }, + if: { $ne: ['$repostId', null] }, then: 'repost', else: { $cond: { - if: { $ne: ['$reply_id', null] }, + if: { $ne: ['$replyId', null] }, then: 'reply', else: 'post' } diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts index 3022b2b002..73a30281b3 100644 --- a/src/server/api/endpoints/aggregation/users/followers.ts +++ b/src/server/api/endpoints/aggregation/users/followers.ts @@ -12,9 +12,9 @@ import Following from '../../../models/following'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Lookup user const user = await User.findOne({ @@ -33,17 +33,17 @@ module.exports = (params) => new Promise(async (res, rej) => { const following = await Following .find({ - followee_id: user._id, + followeeId: user._id, $or: [ - { deleted_at: { $exists: false } }, - { deleted_at: { $gt: startTime } } + { deletedAt: { $exists: false } }, + { deletedAt: { $gt: startTime } } ] }, { _id: false, - follower_id: false, - followee_id: false + followerId: false, + followeeId: false }, { - sort: { created_at: -1 } + sort: { createdAt: -1 } }); const graph = []; @@ -57,7 +57,7 @@ module.exports = (params) => new Promise(async (res, rej) => { // day = day.getTime(); const count = following.filter(f => - f.created_at < day && (f.deleted_at == null || f.deleted_at > day) + f.createdAt < day && (f.deletedAt == null || f.deletedAt > day) ).length; graph.push({ diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts index 92da7e6921..16fc568d59 100644 --- a/src/server/api/endpoints/aggregation/users/following.ts +++ b/src/server/api/endpoints/aggregation/users/following.ts @@ -12,9 +12,9 @@ import Following from '../../../models/following'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Lookup user const user = await User.findOne({ @@ -33,17 +33,17 @@ module.exports = (params) => new Promise(async (res, rej) => { const following = await Following .find({ - follower_id: user._id, + followerId: user._id, $or: [ - { deleted_at: { $exists: false } }, - { deleted_at: { $gt: startTime } } + { deletedAt: { $exists: false } }, + { deletedAt: { $gt: startTime } } ] }, { _id: false, - follower_id: false, - followee_id: false + followerId: false, + followeeId: false }, { - sort: { created_at: -1 } + sort: { createdAt: -1 } }); const graph = []; @@ -56,7 +56,7 @@ module.exports = (params) => new Promise(async (res, rej) => { day = new Date(day.setHours(23)); const count = following.filter(f => - f.created_at < day && (f.deleted_at == null || f.deleted_at > day) + f.createdAt < day && (f.deletedAt == null || f.deletedAt > day) ).length; graph.push({ diff --git a/src/server/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts index c6a75eee39..c988748593 100644 --- a/src/server/api/endpoints/aggregation/users/post.ts +++ b/src/server/api/endpoints/aggregation/users/post.ts @@ -12,9 +12,9 @@ import Post from '../../../models/post'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Lookup user const user = await User.findOne({ @@ -31,25 +31,25 @@ module.exports = (params) => new Promise(async (res, rej) => { const datas = await Post .aggregate([ - { $match: { user_id: user._id } }, + { $match: { userId: user._id } }, { $project: { - repost_id: '$repost_id', - reply_id: '$reply_id', - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + repostId: '$repostId', + replyId: '$replyId', + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } }, type: { $cond: { - if: { $ne: ['$repost_id', null] }, + if: { $ne: ['$repostId', null] }, then: 'repost', else: { $cond: { - if: { $ne: ['$reply_id', null] }, + if: { $ne: ['$replyId', null] }, then: 'reply', else: 'post' } diff --git a/src/server/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts index 0a082ed1b7..60b33e9d1c 100644 --- a/src/server/api/endpoints/aggregation/users/reaction.ts +++ b/src/server/api/endpoints/aggregation/users/reaction.ts @@ -12,9 +12,9 @@ import Reaction from '../../../models/post-reaction'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Lookup user const user = await User.findOne({ @@ -31,15 +31,15 @@ module.exports = (params) => new Promise(async (res, rej) => { const datas = await Reaction .aggregate([ - { $match: { user_id: user._id } }, + { $match: { userId: user._id } }, { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } } }}, { $group: { diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts index 0f688792a7..713078463d 100644 --- a/src/server/api/endpoints/app/create.ts +++ b/src/server/api/endpoints/app/create.ts @@ -13,7 +13,7 @@ import App, { isValidNameId, pack } from '../../models/app'; * parameters: * - $ref: "#/parameters/AccessToken" * - - * name: name_id + * name: nameId * description: Application unique name * in: formData * required: true @@ -40,7 +40,7 @@ import App, { isValidNameId, pack } from '../../models/app'; * type: string * collectionFormat: csv * - - * name: callback_url + * name: callbackUrl * description: URL called back after authentication * in: formData * required: false @@ -66,9 +66,9 @@ import App, { isValidNameId, pack } from '../../models/app'; * @return {Promise} */ module.exports = async (params, user) => new Promise(async (res, rej) => { - // Get 'name_id' parameter - const [nameId, nameIdErr] = $(params.name_id).string().pipe(isValidNameId).$; - if (nameIdErr) return rej('invalid name_id param'); + // Get 'nameId' parameter + const [nameId, nameIdErr] = $(params.nameId).string().pipe(isValidNameId).$; + if (nameIdErr) return rej('invalid nameId param'); // Get 'name' parameter const [name, nameErr] = $(params.name).string().$; @@ -82,24 +82,24 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { const [permission, permissionErr] = $(params.permission).array('string').unique().$; if (permissionErr) return rej('invalid permission param'); - // Get 'callback_url' parameter + // Get 'callbackUrl' parameter // TODO: Check it is valid url - const [callbackUrl = null, callbackUrlErr] = $(params.callback_url).optional.nullable.string().$; - if (callbackUrlErr) return rej('invalid callback_url param'); + const [callbackUrl = null, callbackUrlErr] = $(params.callbackUrl).optional.nullable.string().$; + if (callbackUrlErr) return rej('invalid callbackUrl param'); // Generate secret const secret = rndstr('a-zA-Z0-9', 32); // Create account const app = await App.insert({ - created_at: new Date(), - user_id: user._id, + createdAt: new Date(), + userId: user._id, name: name, - name_id: nameId, - name_id_lower: nameId.toLowerCase(), + nameId: nameId, + nameIdLower: nameId.toLowerCase(), description: description, permission: permission, - callback_url: callbackUrl, + callbackUrl: callbackUrl, secret: secret }); diff --git a/src/server/api/endpoints/app/name_id/available.ts b/src/server/api/endpoints/app/name_id/available.ts index 3d2c710322..6d02b26d2b 100644 --- a/src/server/api/endpoints/app/name_id/available.ts +++ b/src/server/api/endpoints/app/name_id/available.ts @@ -7,12 +7,12 @@ import { isValidNameId } from '../../../models/app'; /** * @swagger - * /app/name_id/available: + * /app/nameId/available: * post: - * summary: Check available name_id on creation an application + * summary: Check available nameId on creation an application * parameters: * - - * name: name_id + * name: nameId * description: Application unique name * in: formData * required: true @@ -25,7 +25,7 @@ import { isValidNameId } from '../../../models/app'; * type: object * properties: * available: - * description: Whether name_id is available + * description: Whether nameId is available * type: boolean * * default: @@ -35,20 +35,20 @@ import { isValidNameId } from '../../../models/app'; */ /** - * Check available name_id of app + * Check available nameId of app * * @param {any} params * @return {Promise} */ module.exports = async (params) => new Promise(async (res, rej) => { - // Get 'name_id' parameter - const [nameId, nameIdErr] = $(params.name_id).string().pipe(isValidNameId).$; - if (nameIdErr) return rej('invalid name_id param'); + // Get 'nameId' parameter + const [nameId, nameIdErr] = $(params.nameId).string().pipe(isValidNameId).$; + if (nameIdErr) return rej('invalid nameId param'); // Get exist const exist = await App .count({ - name_id_lower: nameId.toLowerCase() + nameIdLower: nameId.toLowerCase() }, { limit: 1 }); diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts index 8bc3dda42c..34bb958eee 100644 --- a/src/server/api/endpoints/app/show.ts +++ b/src/server/api/endpoints/app/show.ts @@ -9,15 +9,15 @@ import App, { pack } from '../../models/app'; * /app/show: * post: * summary: Show an application's information - * description: Require app_id or name_id + * description: Require appId or nameId * parameters: * - - * name: app_id + * name: appId * description: Application ID * in: formData * type: string * - - * name: name_id + * name: nameId * description: Application unique name * in: formData * type: string @@ -44,22 +44,22 @@ import App, { pack } from '../../models/app'; * @return {Promise} */ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => { - // Get 'app_id' parameter - const [appId, appIdErr] = $(params.app_id).optional.id().$; - if (appIdErr) return rej('invalid app_id param'); + // Get 'appId' parameter + const [appId, appIdErr] = $(params.appId).optional.id().$; + if (appIdErr) return rej('invalid appId param'); - // Get 'name_id' parameter - const [nameId, nameIdErr] = $(params.name_id).optional.string().$; - if (nameIdErr) return rej('invalid name_id param'); + // Get 'nameId' parameter + const [nameId, nameIdErr] = $(params.nameId).optional.string().$; + if (nameIdErr) return rej('invalid nameId param'); if (appId === undefined && nameId === undefined) { - return rej('app_id or name_id is required'); + return rej('appId or nameId is required'); } // Lookup app const app = appId !== undefined ? await App.findOne({ _id: appId }) - : await App.findOne({ name_id_lower: nameId.toLowerCase() }); + : await App.findOne({ nameIdLower: nameId.toLowerCase() }); if (app === null) { return rej('app not found'); @@ -67,6 +67,6 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => // Send response res(await pack(app, user, { - includeSecret: isSecure && app.user_id.equals(user._id) + includeSecret: isSecure && app.userId.equals(user._id) })); }); diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts index 4ee20a6d25..5a1925144d 100644 --- a/src/server/api/endpoints/auth/accept.ts +++ b/src/server/api/endpoints/auth/accept.ts @@ -56,14 +56,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Fetch exist access token const exist = await AccessToken.findOne({ - app_id: session.app_id, - user_id: user._id, + appId: session.appId, + userId: user._id, }); if (exist === null) { // Lookup app const app = await App.findOne({ - _id: session.app_id + _id: session.appId }); // Generate Hash @@ -73,9 +73,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Insert access token doc await AccessToken.insert({ - created_at: new Date(), - app_id: session.app_id, - user_id: user._id, + createdAt: new Date(), + appId: session.appId, + userId: user._id, token: accessToken, hash: hash }); @@ -84,7 +84,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Update session await AuthSess.update(session._id, { $set: { - user_id: user._id + userId: user._id } }); diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts index dc6a045b6e..180ad83ccd 100644 --- a/src/server/api/endpoints/auth/session/generate.ts +++ b/src/server/api/endpoints/auth/session/generate.ts @@ -14,7 +14,7 @@ import config from '../../../../../conf'; * summary: Generate a session * parameters: * - - * name: app_secret + * name: appSecret * description: App Secret * in: formData * required: true @@ -45,9 +45,9 @@ import config from '../../../../../conf'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'app_secret' parameter - const [appSecret, appSecretErr] = $(params.app_secret).string().$; - if (appSecretErr) return rej('invalid app_secret param'); + // Get 'appSecret' parameter + const [appSecret, appSecretErr] = $(params.appSecret).string().$; + if (appSecretErr) return rej('invalid appSecret param'); // Lookup app const app = await App.findOne({ @@ -63,8 +63,8 @@ module.exports = (params) => new Promise(async (res, rej) => { // Create session token document const doc = await AuthSess.insert({ - created_at: new Date(), - app_id: app._id, + createdAt: new Date(), + appId: app._id, token: token }); diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts index 73ac3185f6..869b714e71 100644 --- a/src/server/api/endpoints/auth/session/show.ts +++ b/src/server/api/endpoints/auth/session/show.ts @@ -23,17 +23,17 @@ import AuthSess, { pack } from '../../../models/auth-session'; * schema: * type: object * properties: - * created_at: + * createdAt: * type: string * format: date-time * description: Date and time of the session creation - * app_id: + * appId: * type: string * description: Application ID * token: * type: string * description: Session Token - * user_id: + * userId: * type: string * description: ID of user who create the session * app: diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts index fc989bf8c2..5d9983af67 100644 --- a/src/server/api/endpoints/auth/session/userkey.ts +++ b/src/server/api/endpoints/auth/session/userkey.ts @@ -14,7 +14,7 @@ import { pack } from '../../../models/user'; * summary: Get an access token(userkey) * parameters: * - - * name: app_secret + * name: appSecret * description: App Secret * in: formData * required: true @@ -50,9 +50,9 @@ import { pack } from '../../../models/user'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'app_secret' parameter - const [appSecret, appSecretErr] = $(params.app_secret).string().$; - if (appSecretErr) return rej('invalid app_secret param'); + // Get 'appSecret' parameter + const [appSecret, appSecretErr] = $(params.appSecret).string().$; + if (appSecretErr) return rej('invalid appSecret param'); // Lookup app const app = await App.findOne({ @@ -71,21 +71,21 @@ module.exports = (params) => new Promise(async (res, rej) => { const session = await AuthSess .findOne({ token: token, - app_id: app._id + appId: app._id }); if (session === null) { return rej('session not found'); } - if (session.user_id == null) { + if (session.userId == null) { return rej('this session is not allowed yet'); } // Lookup access token const accessToken = await AccessToken.findOne({ - app_id: app._id, - user_id: session.user_id + appId: app._id, + userId: session.userId }); // Delete session @@ -101,8 +101,8 @@ module.exports = (params) => new Promise(async (res, rej) => { // Response res({ - access_token: accessToken.token, - user: await pack(session.user_id, null, { + accessToken: accessToken.token, + user: await pack(session.userId, null, { detail: true }) }); diff --git a/src/server/api/endpoints/channels.ts b/src/server/api/endpoints/channels.ts index b9a7d1b788..a4acc06605 100644 --- a/src/server/api/endpoints/channels.ts +++ b/src/server/api/endpoints/channels.ts @@ -16,17 +16,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } // Construct query diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts index 695b4515b3..1dc453c4a5 100644 --- a/src/server/api/endpoints/channels/create.ts +++ b/src/server/api/endpoints/channels/create.ts @@ -20,11 +20,11 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { // Create a channel const channel = await Channel.insert({ - created_at: new Date(), - user_id: user._id, + createdAt: new Date(), + userId: user._id, title: title, index: 0, - watching_count: 1 + watchingCount: 1 }); // Response @@ -32,8 +32,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { // Create Watching await Watching.insert({ - created_at: new Date(), - user_id: user._id, - channel_id: channel._id + createdAt: new Date(), + userId: user._id, + channelId: channel._id }); }); diff --git a/src/server/api/endpoints/channels/posts.ts b/src/server/api/endpoints/channels/posts.ts index d722589c20..348dbb108b 100644 --- a/src/server/api/endpoints/channels/posts.ts +++ b/src/server/api/endpoints/channels/posts.ts @@ -17,22 +17,22 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 1000, limitErr] = $(params.limit).optional.number().range(1, 1000).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } - // Get 'channel_id' parameter - const [channelId, channelIdErr] = $(params.channel_id).id().$; - if (channelIdErr) return rej('invalid channel_id param'); + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).id().$; + if (channelIdErr) return rej('invalid channelId param'); // Fetch channel const channel: IChannel = await Channel.findOne({ @@ -49,7 +49,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }; const query = { - channel_id: channel._id + channelId: channel._id } as any; if (sinceId) { diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts index 332da64675..5874ed18a6 100644 --- a/src/server/api/endpoints/channels/show.ts +++ b/src/server/api/endpoints/channels/show.ts @@ -12,9 +12,9 @@ import Channel, { IChannel, pack } from '../../models/channel'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'channel_id' parameter - const [channelId, channelIdErr] = $(params.channel_id).id().$; - if (channelIdErr) return rej('invalid channel_id param'); + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).id().$; + if (channelIdErr) return rej('invalid channelId param'); // Fetch channel const channel: IChannel = await Channel.findOne({ diff --git a/src/server/api/endpoints/channels/unwatch.ts b/src/server/api/endpoints/channels/unwatch.ts index 19d3be118a..709313bc6e 100644 --- a/src/server/api/endpoints/channels/unwatch.ts +++ b/src/server/api/endpoints/channels/unwatch.ts @@ -13,9 +13,9 @@ import Watching from '../../models/channel-watching'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'channel_id' parameter - const [channelId, channelIdErr] = $(params.channel_id).id().$; - if (channelIdErr) return rej('invalid channel_id param'); + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).id().$; + if (channelIdErr) return rej('invalid channelId param'); //#region Fetch channel const channel = await Channel.findOne({ @@ -29,9 +29,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { //#region Check whether not watching const exist = await Watching.findOne({ - user_id: user._id, - channel_id: channel._id, - deleted_at: { $exists: false } + userId: user._id, + channelId: channel._id, + deletedAt: { $exists: false } }); if (exist === null) { @@ -44,7 +44,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: exist._id }, { $set: { - deleted_at: new Date() + deletedAt: new Date() } }); @@ -54,7 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Decrement watching count Channel.update(channel._id, { $inc: { - watching_count: -1 + watchingCount: -1 } }); }); diff --git a/src/server/api/endpoints/channels/watch.ts b/src/server/api/endpoints/channels/watch.ts index 030e0dd411..df9e70d5c2 100644 --- a/src/server/api/endpoints/channels/watch.ts +++ b/src/server/api/endpoints/channels/watch.ts @@ -13,9 +13,9 @@ import Watching from '../../models/channel-watching'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'channel_id' parameter - const [channelId, channelIdErr] = $(params.channel_id).id().$; - if (channelIdErr) return rej('invalid channel_id param'); + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).id().$; + if (channelIdErr) return rej('invalid channelId param'); //#region Fetch channel const channel = await Channel.findOne({ @@ -29,9 +29,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { //#region Check whether already watching const exist = await Watching.findOne({ - user_id: user._id, - channel_id: channel._id, - deleted_at: { $exists: false } + userId: user._id, + channelId: channel._id, + deletedAt: { $exists: false } }); if (exist !== null) { @@ -41,9 +41,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Create Watching await Watching.insert({ - created_at: new Date(), - user_id: user._id, - channel_id: channel._id + createdAt: new Date(), + userId: user._id, + channelId: channel._id }); // Send response @@ -52,7 +52,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Increment watching count Channel.update(channel._id, { $inc: { - watching_count: 1 + watchingCount: 1 } }); }); diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts index d92473633a..eb21853916 100644 --- a/src/server/api/endpoints/drive.ts +++ b/src/server/api/endpoints/drive.ts @@ -14,7 +14,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Calculate drive usage const usage = ((await DriveFile .aggregate([ - { $match: { 'metadata.user_id': user._id } }, + { $match: { 'metadata.userId': user._id } }, { $project: { length: true @@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }).usage; res({ - capacity: user.drive_capacity, + capacity: user.driveCapacity, usage: usage }); }); diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts index 89915331ea..f982ef62e0 100644 --- a/src/server/api/endpoints/drive/files.ts +++ b/src/server/api/endpoints/drive/files.ts @@ -17,22 +17,22 @@ module.exports = async (params, user, app) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) throw 'invalid limit param'; - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) throw 'invalid since_id param'; + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) throw 'invalid sinceId param'; - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) throw 'invalid until_id param'; + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) throw 'invalid untilId param'; - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - throw 'cannot set since_id and until_id'; + throw 'cannot set sinceId and untilId'; } - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) throw 'invalid folder_id param'; + // Get 'folderId' parameter + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) throw 'invalid folderId param'; // Get 'type' parameter const [type, typeErr] = $(params.type).optional.string().match(/^[a-zA-Z\/\-\*]+$/).$; @@ -43,8 +43,8 @@ module.exports = async (params, user, app) => { _id: -1 }; const query = { - 'metadata.user_id': user._id, - 'metadata.folder_id': folderId + 'metadata.userId': user._id, + 'metadata.folderId': folderId } as any; if (sinceId) { sort._id = 1; diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index db801b61fe..2cd89a8fac 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -33,9 +33,9 @@ module.exports = async (file, params, user): Promise => { name = null; } - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) throw 'invalid folder_id param'; + // Get 'folderId' parameter + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) throw 'invalid folderId param'; try { // Create file diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts index e026afe936..47ce891305 100644 --- a/src/server/api/endpoints/drive/files/find.ts +++ b/src/server/api/endpoints/drive/files/find.ts @@ -16,16 +16,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [name, nameErr] = $(params.name).string().$; if (nameErr) return rej('invalid name param'); - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); + // Get 'folderId' parameter + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) return rej('invalid folderId param'); // Issue query const files = await DriveFile .find({ filename: name, - 'metadata.user_id': user._id, - 'metadata.folder_id': folderId + 'metadata.userId': user._id, + 'metadata.folderId': folderId }); // Serialize diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts index 21664f7ba4..63920db7fc 100644 --- a/src/server/api/endpoints/drive/files/show.ts +++ b/src/server/api/endpoints/drive/files/show.ts @@ -12,15 +12,15 @@ import DriveFile, { pack } from '../../../models/drive-file'; * @return {Promise} */ module.exports = async (params, user) => { - // Get 'file_id' parameter - const [fileId, fileIdErr] = $(params.file_id).id().$; - if (fileIdErr) throw 'invalid file_id param'; + // Get 'fileId' parameter + const [fileId, fileIdErr] = $(params.fileId).id().$; + if (fileIdErr) throw 'invalid fileId param'; // Fetch file const file = await DriveFile .findOne({ _id: fileId, - 'metadata.user_id': user._id + 'metadata.userId': user._id }); if (file === null) { diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts index 83da462113..bfad45b0a2 100644 --- a/src/server/api/endpoints/drive/files/update.ts +++ b/src/server/api/endpoints/drive/files/update.ts @@ -14,15 +14,15 @@ import { publishDriveStream } from '../../../event'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'file_id' parameter - const [fileId, fileIdErr] = $(params.file_id).id().$; - if (fileIdErr) return rej('invalid file_id param'); + // Get 'fileId' parameter + const [fileId, fileIdErr] = $(params.fileId).id().$; + if (fileIdErr) return rej('invalid fileId param'); // Fetch file const file = await DriveFile .findOne({ _id: fileId, - 'metadata.user_id': user._id + 'metadata.userId': user._id }); if (file === null) { @@ -34,33 +34,33 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (nameErr) return rej('invalid name param'); if (name) file.filename = name; - // Get 'folder_id' parameter - const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); + // Get 'folderId' parameter + const [folderId, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) return rej('invalid folderId param'); if (folderId !== undefined) { if (folderId === null) { - file.metadata.folder_id = null; + file.metadata.folderId = null; } else { // Fetch folder const folder = await DriveFolder .findOne({ _id: folderId, - user_id: user._id + userId: user._id }); if (folder === null) { return rej('folder-not-found'); } - file.metadata.folder_id = folder._id; + file.metadata.folderId = folder._id; } } await DriveFile.update(file._id, { $set: { filename: file.filename, - 'metadata.folder_id': file.metadata.folder_id + 'metadata.folderId': file.metadata.folderId } }); diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts index 346633c616..1a4ce0bf08 100644 --- a/src/server/api/endpoints/drive/files/upload_from_url.ts +++ b/src/server/api/endpoints/drive/files/upload_from_url.ts @@ -18,9 +18,9 @@ module.exports = async (params, user): Promise => { const [url, urlErr] = $(params.url).string().$; if (urlErr) throw 'invalid url param'; - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) throw 'invalid folder_id param'; + // Get 'folderId' parameter + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) throw 'invalid folderId param'; return pack(await uploadFromUrl(url, user, folderId)); }; diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts index 428bde3507..c314837f72 100644 --- a/src/server/api/endpoints/drive/folders.ts +++ b/src/server/api/endpoints/drive/folders.ts @@ -17,30 +17,30 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); + // Get 'folderId' parameter + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) return rej('invalid folderId param'); // Construct query const sort = { _id: -1 }; const query = { - user_id: user._id, - parent_id: folderId + userId: user._id, + parentId: folderId } as any; if (sinceId) { sort._id = 1; diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts index 03f396ddc9..564558606d 100644 --- a/src/server/api/endpoints/drive/folders/create.ts +++ b/src/server/api/endpoints/drive/folders/create.ts @@ -17,9 +17,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [name = '無題のフォルダー', nameErr] = $(params.name).optional.string().pipe(isValidFolderName).$; if (nameErr) return rej('invalid name param'); - // Get 'parent_id' parameter - const [parentId = null, parentIdErr] = $(params.parent_id).optional.nullable.id().$; - if (parentIdErr) return rej('invalid parent_id param'); + // Get 'parentId' parameter + const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$; + if (parentIdErr) return rej('invalid parentId param'); // If the parent folder is specified let parent = null; @@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { parent = await DriveFolder .findOne({ _id: parentId, - user_id: user._id + userId: user._id }); if (parent === null) { @@ -38,10 +38,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Create folder const folder = await DriveFolder.insert({ - created_at: new Date(), + createdAt: new Date(), name: name, - parent_id: parent !== null ? parent._id : null, - user_id: user._id + parentId: parent !== null ? parent._id : null, + userId: user._id }); // Serialize diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts index fc84766bc8..f46aaedd32 100644 --- a/src/server/api/endpoints/drive/folders/find.ts +++ b/src/server/api/endpoints/drive/folders/find.ts @@ -16,16 +16,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [name, nameErr] = $(params.name).string().$; if (nameErr) return rej('invalid name param'); - // Get 'parent_id' parameter - const [parentId = null, parentIdErr] = $(params.parent_id).optional.nullable.id().$; - if (parentIdErr) return rej('invalid parent_id param'); + // Get 'parentId' parameter + const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$; + if (parentIdErr) return rej('invalid parentId param'); // Issue query const folders = await DriveFolder .find({ name: name, - user_id: user._id, - parent_id: parentId + userId: user._id, + parentId: parentId }); // Serialize diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts index e07d14d20d..a6d7e86df1 100644 --- a/src/server/api/endpoints/drive/folders/show.ts +++ b/src/server/api/endpoints/drive/folders/show.ts @@ -12,15 +12,15 @@ import DriveFolder, { pack } from '../../../models/drive-folder'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'folder_id' parameter - const [folderId, folderIdErr] = $(params.folder_id).id().$; - if (folderIdErr) return rej('invalid folder_id param'); + // Get 'folderId' parameter + const [folderId, folderIdErr] = $(params.folderId).id().$; + if (folderIdErr) return rej('invalid folderId param'); // Get folder const folder = await DriveFolder .findOne({ _id: folderId, - user_id: user._id + userId: user._id }); if (folder === null) { diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts index d3df8bdae5..fcfd241241 100644 --- a/src/server/api/endpoints/drive/folders/update.ts +++ b/src/server/api/endpoints/drive/folders/update.ts @@ -13,15 +13,15 @@ import { publishDriveStream } from '../../../event'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'folder_id' parameter - const [folderId, folderIdErr] = $(params.folder_id).id().$; - if (folderIdErr) return rej('invalid folder_id param'); + // Get 'folderId' parameter + const [folderId, folderIdErr] = $(params.folderId).id().$; + if (folderIdErr) return rej('invalid folderId param'); // Fetch folder const folder = await DriveFolder .findOne({ _id: folderId, - user_id: user._id + userId: user._id }); if (folder === null) { @@ -33,18 +33,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (nameErr) return rej('invalid name param'); if (name) folder.name = name; - // Get 'parent_id' parameter - const [parentId, parentIdErr] = $(params.parent_id).optional.nullable.id().$; - if (parentIdErr) return rej('invalid parent_id param'); + // Get 'parentId' parameter + const [parentId, parentIdErr] = $(params.parentId).optional.nullable.id().$; + if (parentIdErr) return rej('invalid parentId param'); if (parentId !== undefined) { if (parentId === null) { - folder.parent_id = null; + folder.parentId = null; } else { // Get parent folder const parent = await DriveFolder .findOne({ _id: parentId, - user_id: user._id + userId: user._id }); if (parent === null) { @@ -58,25 +58,25 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: folderId }, { _id: true, - parent_id: true + parentId: true }); if (folder2._id.equals(folder._id)) { return true; - } else if (folder2.parent_id) { - return await checkCircle(folder2.parent_id); + } else if (folder2.parentId) { + return await checkCircle(folder2.parentId); } else { return false; } } - if (parent.parent_id !== null) { - if (await checkCircle(parent.parent_id)) { + if (parent.parentId !== null) { + if (await checkCircle(parent.parentId)) { return rej('detected-circular-definition'); } } - folder.parent_id = parent._id; + folder.parentId = parent._id; } } @@ -84,7 +84,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { DriveFolder.update(folder._id, { $set: { name: folder.name, - parent_id: folder.parent_id + parentId: folder.parentId } }); diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts index 8352c7dd4c..71db38f3b3 100644 --- a/src/server/api/endpoints/drive/stream.ts +++ b/src/server/api/endpoints/drive/stream.ts @@ -16,17 +16,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } // Get 'type' parameter @@ -38,7 +38,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: -1 }; const query = { - 'metadata.user_id': user._id + 'metadata.userId': user._id } as any; if (sinceId) { sort._id = 1; diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index 767b837b35..983d8040f9 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -17,9 +17,9 @@ import event from '../../event'; module.exports = (params, user) => new Promise(async (res, rej) => { const follower = user; - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // 自分自身 if (user._id.equals(userId)) { @@ -42,9 +42,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Check if already following const exist = await Following.findOne({ - follower_id: follower._id, - followee_id: followee._id, - deleted_at: { $exists: false } + followerId: follower._id, + followeeId: followee._id, + deletedAt: { $exists: false } }); if (exist !== null) { @@ -53,9 +53,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Create following await Following.insert({ - created_at: new Date(), - follower_id: follower._id, - followee_id: followee._id + createdAt: new Date(), + followerId: follower._id, + followeeId: followee._id }); // Send response @@ -64,14 +64,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Increment following count User.update(follower._id, { $inc: { - following_count: 1 + followingCount: 1 } }); // Increment followers count User.update({ _id: followee._id }, { $inc: { - followers_count: 1 + followersCount: 1 } }); diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts index 64b9a8cecb..25eba8b262 100644 --- a/src/server/api/endpoints/following/delete.ts +++ b/src/server/api/endpoints/following/delete.ts @@ -16,9 +16,9 @@ import event from '../../event'; module.exports = (params, user) => new Promise(async (res, rej) => { const follower = user; - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Check if the followee is yourself if (user._id.equals(userId)) { @@ -41,9 +41,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Check not following const exist = await Following.findOne({ - follower_id: follower._id, - followee_id: followee._id, - deleted_at: { $exists: false } + followerId: follower._id, + followeeId: followee._id, + deletedAt: { $exists: false } }); if (exist === null) { @@ -55,7 +55,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: exist._id }, { $set: { - deleted_at: new Date() + deletedAt: new Date() } }); @@ -65,14 +65,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Decrement following count User.update({ _id: follower._id }, { $inc: { - following_count: -1 + followingCount: -1 } }); // Decrement followers count User.update({ _id: followee._id }, { $inc: { - followers_count: -1 + followersCount: -1 } }); diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts index 32b0382faf..f5e92b4ded 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -22,7 +22,7 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => // Update lastUsedAt User.update({ _id: user._id }, { $set: { - 'account.last_used_at': new Date() + 'account.lastUsedAt': new Date() } }); }); diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts index 0f1db73829..d61ebbe6f9 100644 --- a/src/server/api/endpoints/i/2fa/done.ts +++ b/src/server/api/endpoints/i/2fa/done.ts @@ -12,12 +12,12 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { const _token = token.replace(/\s/g, ''); - if (user.two_factor_temp_secret == null) { + if (user.twoFactorTempSecret == null) { return rej('二段階認証の設定が開始されていません'); } const verified = (speakeasy as any).totp.verify({ - secret: user.two_factor_temp_secret, + secret: user.twoFactorTempSecret, encoding: 'base32', token: _token }); @@ -28,8 +28,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { await User.update(user._id, { $set: { - 'account.two_factor_secret': user.two_factor_temp_secret, - 'account.two_factor_enabled': true + 'account.twoFactorSecret': user.twoFactorTempSecret, + 'account.twoFactorEnabled': true } }); diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts index e2cc1487b8..0b49ad8821 100644 --- a/src/server/api/endpoints/i/2fa/register.ts +++ b/src/server/api/endpoints/i/2fa/register.ts @@ -27,7 +27,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { await User.update(user._id, { $set: { - two_factor_temp_secret: secret.base32 + twoFactorTempSecret: secret.base32 } }); diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts index c43f9ccc44..0221ecb96d 100644 --- a/src/server/api/endpoints/i/2fa/unregister.ts +++ b/src/server/api/endpoints/i/2fa/unregister.ts @@ -19,8 +19,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { await User.update(user._id, { $set: { - 'account.two_factor_secret': null, - 'account.two_factor_enabled': false + 'account.twoFactorSecret': null, + 'account.twoFactorEnabled': false } }); diff --git a/src/server/api/endpoints/i/appdata/get.ts b/src/server/api/endpoints/i/appdata/get.ts index 571208d46c..0b34643f75 100644 --- a/src/server/api/endpoints/i/appdata/get.ts +++ b/src/server/api/endpoints/i/appdata/get.ts @@ -25,8 +25,8 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { select[`data.${key}`] = true; } const appdata = await Appdata.findOne({ - app_id: app._id, - user_id: user._id + appId: app._id, + userId: user._id }, { fields: select }); diff --git a/src/server/api/endpoints/i/appdata/set.ts b/src/server/api/endpoints/i/appdata/set.ts index 2804a14cb3..1e3232ce3d 100644 --- a/src/server/api/endpoints/i/appdata/set.ts +++ b/src/server/api/endpoints/i/appdata/set.ts @@ -43,11 +43,11 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { } await Appdata.update({ - app_id: app._id, - user_id: user._id + appId: app._id, + userId: user._id }, Object.assign({ - app_id: app._id, - user_id: user._id + appId: app._id, + userId: user._id }, { $set: set }), { diff --git a/src/server/api/endpoints/i/authorized_apps.ts b/src/server/api/endpoints/i/authorized_apps.ts index 40ce7a68c8..5a38d7c18f 100644 --- a/src/server/api/endpoints/i/authorized_apps.ts +++ b/src/server/api/endpoints/i/authorized_apps.ts @@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Get tokens const tokens = await AccessToken .find({ - user_id: user._id + userId: user._id }, { limit: limit, skip: offset, @@ -39,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(tokens.map(async token => - await pack(token.app_id)))); + await pack(token.appId)))); }); diff --git a/src/server/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts index 88fb36b1fb..e3b0127e7d 100644 --- a/src/server/api/endpoints/i/change_password.ts +++ b/src/server/api/endpoints/i/change_password.ts @@ -13,13 +13,13 @@ import User from '../../models/user'; * @return {Promise} */ module.exports = async (params, user) => new Promise(async (res, rej) => { - // Get 'current_password' parameter - const [currentPassword, currentPasswordErr] = $(params.current_password).string().$; - if (currentPasswordErr) return rej('invalid current_password param'); + // Get 'currentPasword' parameter + const [currentPassword, currentPasswordErr] = $(params.currentPasword).string().$; + if (currentPasswordErr) return rej('invalid currentPasword param'); - // Get 'new_password' parameter - const [newPassword, newPasswordErr] = $(params.new_password).string().$; - if (newPasswordErr) return rej('invalid new_password param'); + // Get 'newPassword' parameter + const [newPassword, newPasswordErr] = $(params.newPassword).string().$; + if (newPasswordErr) return rej('invalid newPassword param'); // Compare password const same = await bcrypt.compare(currentPassword, user.account.password); diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts index eb464cf0f0..9f8becf218 100644 --- a/src/server/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Get favorites const favorites = await Favorite .find({ - user_id: user._id + userId: user._id }, { limit: limit, skip: offset, @@ -39,6 +39,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(favorites.map(async favorite => - await pack(favorite.post) + await pack(favorite.postId) ))); }); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index 688039a0dd..7119bf6ea3 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -21,9 +21,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { $(params.following).optional.boolean().$; if (followingError) return rej('invalid following param'); - // Get 'mark_as_read' parameter - const [markAsRead = true, markAsReadErr] = $(params.mark_as_read).optional.boolean().$; - if (markAsReadErr) return rej('invalid mark_as_read param'); + // Get 'markAsRead' parameter + const [markAsRead = true, markAsReadErr] = $(params.markAsRead).optional.boolean().$; + if (markAsReadErr) return rej('invalid markAsRead param'); // Get 'type' parameter const [type, typeErr] = $(params.type).optional.array('string').unique().$; @@ -33,29 +33,29 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } const mute = await Mute.find({ - muter_id: user._id, - deleted_at: { $exists: false } + muterId: user._id, + deletedAt: { $exists: false } }); const query = { - notifiee_id: user._id, + notifieeId: user._id, $and: [{ - notifier_id: { - $nin: mute.map(m => m.mutee_id) + notifierId: { + $nin: mute.map(m => m.muteeId) } }] } as any; @@ -69,7 +69,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const followingIds = await getFriends(user._id); query.$and.push({ - notifier_id: { + notifierId: { $in: followingIds } }); diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts index ff546fc2bd..886a3edeb5 100644 --- a/src/server/api/endpoints/i/pin.ts +++ b/src/server/api/endpoints/i/pin.ts @@ -14,14 +14,14 @@ import { pack } from '../../models/user'; * @return {Promise} */ module.exports = async (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Fetch pinee const post = await Post.findOne({ _id: postId, - user_id: user._id + userId: user._id }); if (post === null) { @@ -30,7 +30,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { await User.update(user._id, { $set: { - pinned_post_id: post._id + pinnedPostId: post._id } }); diff --git a/src/server/api/endpoints/i/signin_history.ts b/src/server/api/endpoints/i/signin_history.ts index 859e81653d..a4ba22790c 100644 --- a/src/server/api/endpoints/i/signin_history.ts +++ b/src/server/api/endpoints/i/signin_history.ts @@ -16,21 +16,21 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } const query = { - user_id: user._id + userId: user._id } as any; const sort = { diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 3d52de2cc5..8147b1bba4 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -36,34 +36,34 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re if (birthdayErr) return rej('invalid birthday param'); if (birthday !== undefined) user.account.profile.birthday = birthday; - // Get 'avatar_id' parameter - const [avatarId, avatarIdErr] = $(params.avatar_id).optional.id().$; - if (avatarIdErr) return rej('invalid avatar_id param'); - if (avatarId) user.avatar_id = avatarId; + // Get 'avatarId' parameter + const [avatarId, avatarIdErr] = $(params.avatarId).optional.id().$; + if (avatarIdErr) return rej('invalid avatarId param'); + if (avatarId) user.avatarId = avatarId; - // Get 'banner_id' parameter - const [bannerId, bannerIdErr] = $(params.banner_id).optional.id().$; - if (bannerIdErr) return rej('invalid banner_id param'); - if (bannerId) user.banner_id = bannerId; + // Get 'bannerId' parameter + const [bannerId, bannerIdErr] = $(params.bannerId).optional.id().$; + if (bannerIdErr) return rej('invalid bannerId param'); + if (bannerId) user.bannerId = bannerId; - // Get 'is_bot' parameter - const [isBot, isBotErr] = $(params.is_bot).optional.boolean().$; - if (isBotErr) return rej('invalid is_bot param'); - if (isBot != null) user.account.is_bot = isBot; + // Get 'isBot' parameter + const [isBot, isBotErr] = $(params.isBot).optional.boolean().$; + if (isBotErr) return rej('invalid isBot param'); + if (isBot != null) user.account.isBot = isBot; - // Get 'auto_watch' parameter - const [autoWatch, autoWatchErr] = $(params.auto_watch).optional.boolean().$; - if (autoWatchErr) return rej('invalid auto_watch param'); - if (autoWatch != null) user.account.settings.auto_watch = autoWatch; + // 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; await User.update(user._id, { $set: { name: user.name, description: user.description, - avatar_id: user.avatar_id, - banner_id: user.banner_id, + avatarId: user.avatarId, + bannerId: user.bannerId, 'account.profile': user.account.profile, - 'account.is_bot': user.account.is_bot, + 'account.isBot': user.account.isBot, 'account.settings': user.account.settings } }); diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts index c772ed5dc3..a0bef5e595 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.client_settings.${name}`] = value; + x[`account.clientSettings.${name}`] = value; await User.update(user._id, { $set: x }); // Serialize - user.account.client_settings[name] = value; + user.account.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 9ce44e25ee..151c3e205f 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.client_settings.home': home + 'account.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.client_settings.home; + const _home = user.account.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.client_settings.home': _home + 'account.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 1daddf42b9..a8436b940f 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.client_settings.mobile_home': home + 'account.clientSettings.mobile_home': 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.client_settings.mobile_home || []; + const _home = user.account.clientSettings.mobile_home || []; 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.client_settings.mobile_home': _home + 'account.clientSettings.mobile_home': _home } }); diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts index 1683ca7a89..2bf3ed996d 100644 --- a/src/server/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -19,25 +19,25 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); const mute = await Mute.find({ - muter_id: user._id, - deleted_at: { $exists: false } + muterId: user._id, + deletedAt: { $exists: false } }); // Get history const history = await History .find({ - user_id: user._id, - partner: { - $nin: mute.map(m => m.mutee_id) + userId: user._id, + partnerId: { + $nin: mute.map(m => m.muteeId) } }, { limit: limit, sort: { - updated_at: -1 + updatedAt: -1 } }); // Serialize res(await Promise.all(history.map(async h => - await pack(h.message, user)))); + await pack(h.messageId, user)))); }); diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts index 67ba5e9d6d..dd80e41d03 100644 --- a/src/server/api/endpoints/messaging/messages.ts +++ b/src/server/api/endpoints/messaging/messages.ts @@ -15,9 +15,9 @@ import read from '../../common/read-messaging-message'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [recipientId, recipientIdErr] = $(params.user_id).id().$; - if (recipientIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [recipientId, recipientIdErr] = $(params.userId).id().$; + if (recipientIdErr) return rej('invalid userId param'); // Fetch recipient const recipient = await User.findOne({ @@ -32,34 +32,34 @@ module.exports = (params, user) => new Promise(async (res, rej) => { return rej('user not found'); } - // Get 'mark_as_read' parameter - const [markAsRead = true, markAsReadErr] = $(params.mark_as_read).optional.boolean().$; - if (markAsReadErr) return rej('invalid mark_as_read param'); + // Get 'markAsRead' parameter + const [markAsRead = true, markAsReadErr] = $(params.markAsRead).optional.boolean().$; + if (markAsReadErr) return rej('invalid markAsRead param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } const query = { $or: [{ - user_id: user._id, - recipient_id: recipient._id + userId: user._id, + recipientId: recipient._id }, { - user_id: recipient._id, - recipient_id: user._id + userId: recipient._id, + recipientId: user._id }] } as any; diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index 5184b2bd34..4edd726552 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -21,9 +21,9 @@ import config from '../../../../../conf'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [recipientId, recipientIdErr] = $(params.user_id).id().$; - if (recipientIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [recipientId, recipientIdErr] = $(params.userId).id().$; + if (recipientIdErr) return rej('invalid userId param'); // Myself if (recipientId.equals(user._id)) { @@ -47,15 +47,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; if (textErr) return rej('invalid text'); - // Get 'file_id' parameter - const [fileId, fileIdErr] = $(params.file_id).optional.id().$; - if (fileIdErr) return rej('invalid file_id param'); + // Get 'fileId' parameter + const [fileId, fileIdErr] = $(params.fileId).optional.id().$; + if (fileIdErr) return rej('invalid fileId param'); let file = null; if (fileId !== undefined) { file = await DriveFile.findOne({ _id: fileId, - 'metadata.user_id': user._id + 'metadata.userId': user._id }); if (file === null) { @@ -70,12 +70,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // メッセージを作成 const message = await Message.insert({ - created_at: new Date(), - file_id: file ? file._id : undefined, - recipient_id: recipient._id, + createdAt: new Date(), + fileId: file ? file._id : undefined, + recipientId: recipient._id, text: text ? text : undefined, - user_id: user._id, - is_read: false + userId: user._id, + isRead: false }); // Serialize @@ -85,32 +85,32 @@ module.exports = (params, user) => new Promise(async (res, rej) => { res(messageObj); // 自分のストリーム - publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj); - publishMessagingIndexStream(message.user_id, 'message', messageObj); - publishUserStream(message.user_id, 'messaging_message', messageObj); + publishMessagingStream(message.userId, message.recipientId, 'message', messageObj); + publishMessagingIndexStream(message.userId, 'message', messageObj); + publishUserStream(message.userId, 'messaging_message', messageObj); // 相手のストリーム - publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj); - publishMessagingIndexStream(message.recipient_id, 'message', messageObj); - publishUserStream(message.recipient_id, 'messaging_message', messageObj); + publishMessagingStream(message.recipientId, message.userId, 'message', messageObj); + publishMessagingIndexStream(message.recipientId, 'message', messageObj); + publishUserStream(message.recipientId, 'messaging_message', messageObj); // 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する setTimeout(async () => { - const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true }); - if (!freshMessage.is_read) { + const freshMessage = await Message.findOne({ _id: message._id }, { isRead: true }); + if (!freshMessage.isRead) { //#region ただしミュートされているなら発行しない const mute = await Mute.find({ - muter_id: recipient._id, - deleted_at: { $exists: false } + muterId: recipient._id, + deletedAt: { $exists: false } }); - const mutedUserIds = mute.map(m => m.mutee_id.toString()); + const mutedUserIds = mute.map(m => m.muteeId.toString()); if (mutedUserIds.indexOf(user._id.toString()) != -1) { return; } //#endregion - publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj); - pushSw(message.recipient_id, 'unread_messaging_message', messageObj); + publishUserStream(message.recipientId, 'unread_messaging_message', messageObj); + pushSw(message.recipientId, 'unread_messaging_message', messageObj); } }, 3000); @@ -130,26 +130,26 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // 履歴作成(自分) History.update({ - user_id: user._id, - partner: recipient._id + userId: user._id, + partnerId: recipient._id }, { - updated_at: new Date(), - user_id: user._id, - partner: recipient._id, - message: message._id + updatedAt: new Date(), + userId: user._id, + partnerId: recipient._id, + messageId: message._id }, { upsert: true }); // 履歴作成(相手) History.update({ - user_id: recipient._id, - partner: user._id + userId: recipient._id, + partnerId: user._id }, { - updated_at: new Date(), - user_id: recipient._id, - partner: user._id, - message: message._id + updatedAt: new Date(), + userId: recipient._id, + partnerId: user._id, + messageId: message._id }, { upsert: true }); diff --git a/src/server/api/endpoints/messaging/unread.ts b/src/server/api/endpoints/messaging/unread.ts index c4326e1d22..f7f4047b67 100644 --- a/src/server/api/endpoints/messaging/unread.ts +++ b/src/server/api/endpoints/messaging/unread.ts @@ -13,18 +13,18 @@ import Mute from '../../models/mute'; */ module.exports = (params, user) => new Promise(async (res, rej) => { const mute = await Mute.find({ - muter_id: user._id, - deleted_at: { $exists: false } + muterId: user._id, + deletedAt: { $exists: false } }); - const mutedUserIds = mute.map(m => m.mutee_id); + const mutedUserIds = mute.map(m => m.muteeId); const count = await Message .count({ - user_id: { + userId: { $nin: mutedUserIds }, - recipient_id: user._id, - is_read: false + recipientId: user._id, + isRead: false }); res({ diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 10625ec66f..cb47ede57b 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -53,7 +53,6 @@ module.exports = (params) => new Promise(async (res, rej) => { model: os.cpus()[0].model, cores: os.cpus().length }, - top_image: meta.top_image, broadcasts: meta.broadcasts }); }); diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts index f99b40d32e..e860235086 100644 --- a/src/server/api/endpoints/mute/create.ts +++ b/src/server/api/endpoints/mute/create.ts @@ -15,9 +15,9 @@ import Mute from '../../models/mute'; module.exports = (params, user) => new Promise(async (res, rej) => { const muter = user; - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // 自分自身 if (user._id.equals(userId)) { @@ -40,9 +40,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Check if already muting const exist = await Mute.findOne({ - muter_id: muter._id, - mutee_id: mutee._id, - deleted_at: { $exists: false } + muterId: muter._id, + muteeId: mutee._id, + deletedAt: { $exists: false } }); if (exist !== null) { @@ -51,9 +51,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Create mute await Mute.insert({ - created_at: new Date(), - muter_id: muter._id, - mutee_id: mutee._id, + createdAt: new Date(), + muterId: muter._id, + muteeId: mutee._id, }); // Send response diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts index 36e2fd101a..7e361b4792 100644 --- a/src/server/api/endpoints/mute/delete.ts +++ b/src/server/api/endpoints/mute/delete.ts @@ -15,9 +15,9 @@ import Mute from '../../models/mute'; module.exports = (params, user) => new Promise(async (res, rej) => { const muter = user; - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Check if the mutee is yourself if (user._id.equals(userId)) { @@ -40,9 +40,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Check not muting const exist = await Mute.findOne({ - muter_id: muter._id, - mutee_id: mutee._id, - deleted_at: { $exists: false } + muterId: muter._id, + muteeId: mutee._id, + deletedAt: { $exists: false } }); if (exist === null) { @@ -54,7 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: exist._id }, { $set: { - deleted_at: new Date() + deletedAt: new Date() } }); diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts index 19e3b157e6..3401fba64d 100644 --- a/src/server/api/endpoints/mute/list.ts +++ b/src/server/api/endpoints/mute/list.ts @@ -28,15 +28,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Construct query const query = { - muter_id: me._id, - deleted_at: { $exists: false } + muterId: me._id, + deletedAt: { $exists: false } } as any; if (iknow) { // Get my friends const myFriends = await getFriends(me._id); - query.mutee_id = { + query.muteeId = { $in: myFriends }; } @@ -63,7 +63,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize const users = await Promise.all(mutes.map(async m => - await pack(m.mutee_id, me, { detail: true }))); + await pack(m.muteeId, me, { detail: true }))); // Response res({ diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts index b236190506..bc1290cac6 100644 --- a/src/server/api/endpoints/my/apps.ts +++ b/src/server/api/endpoints/my/apps.ts @@ -21,7 +21,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (offsetErr) return rej('invalid offset param'); const query = { - user_id: user._id + userId: user._id }; // Execute query diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts index 845d6b29ce..8f9719fff6 100644 --- a/src/server/api/endpoints/notifications/get_unread_count.ts +++ b/src/server/api/endpoints/notifications/get_unread_count.ts @@ -13,18 +13,18 @@ import Mute from '../../models/mute'; */ module.exports = (params, user) => new Promise(async (res, rej) => { const mute = await Mute.find({ - muter_id: user._id, - deleted_at: { $exists: false } + muterId: user._id, + deletedAt: { $exists: false } }); - const mutedUserIds = mute.map(m => m.mutee_id); + const mutedUserIds = mute.map(m => m.muteeId); const count = await Notification .count({ - notifiee_id: user._id, - notifier_id: { + notifieeId: user._id, + notifierId: { $nin: mutedUserIds }, - is_read: false + isRead: false }); res({ diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts index 3550e344c4..693de3d0ef 100644 --- a/src/server/api/endpoints/notifications/mark_as_read_all.ts +++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts @@ -14,11 +14,11 @@ import event from '../../event'; module.exports = (params, user) => new Promise(async (res, rej) => { // Update documents await Notification.update({ - notifiee_id: user._id, - is_read: false + notifieeId: user._id, + isRead: false }, { $set: { - is_read: true + isRead: true } }, { multi: true diff --git a/src/server/api/endpoints/othello/games.ts b/src/server/api/endpoints/othello/games.ts index 2a6bbb4043..37fa384189 100644 --- a/src/server/api/endpoints/othello/games.ts +++ b/src/server/api/endpoints/othello/games.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import Game, { pack } from '../../models/othello-game'; +import OthelloGame, { pack } from '../../models/othello-game'; module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'my' parameter @@ -10,28 +10,28 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } const q: any = my ? { - is_started: true, + isStarted: true, $or: [{ - user1_id: user._id + user1Id: user._id }, { - user2_id: user._id + user2Id: user._id }] } : { - is_started: true + isStarted: true }; const sort = { @@ -50,7 +50,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Fetch games - const games = await Game.find(q, { + const games = await OthelloGame.find(q, { sort, limit }); diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts index 2b0db4dd00..f9084682fa 100644 --- a/src/server/api/endpoints/othello/games/show.ts +++ b/src/server/api/endpoints/othello/games/show.ts @@ -1,22 +1,22 @@ import $ from 'cafy'; -import Game, { pack } from '../../../models/othello-game'; +import OthelloGame, { pack } from '../../../models/othello-game'; import Othello from '../../../../common/othello/core'; module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'game_id' parameter - const [gameId, gameIdErr] = $(params.game_id).id().$; - if (gameIdErr) return rej('invalid game_id param'); + // Get 'gameId' parameter + const [gameId, gameIdErr] = $(params.gameId).id().$; + if (gameIdErr) return rej('invalid gameId param'); - const game = await Game.findOne({ _id: gameId }); + const game = await OthelloGame.findOne({ _id: gameId }); if (game == null) { return rej('game not found'); } const o = new Othello(game.settings.map, { - isLlotheo: game.settings.is_llotheo, - canPutEverywhere: game.settings.can_put_everywhere, - loopedBoard: game.settings.looped_board + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard }); game.logs.forEach(log => { diff --git a/src/server/api/endpoints/othello/invitations.ts b/src/server/api/endpoints/othello/invitations.ts index 02fb421fbc..f6e0071a6c 100644 --- a/src/server/api/endpoints/othello/invitations.ts +++ b/src/server/api/endpoints/othello/invitations.ts @@ -3,7 +3,7 @@ import Matching, { pack as packMatching } from '../../models/othello-matching'; module.exports = (params, user) => new Promise(async (res, rej) => { // Find session const invitations = await Matching.find({ - child_id: user._id + childId: user._id }, { sort: { _id: -1 diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts index b73e105ef0..f503c5834c 100644 --- a/src/server/api/endpoints/othello/match.ts +++ b/src/server/api/endpoints/othello/match.ts @@ -1,24 +1,24 @@ import $ from 'cafy'; import Matching, { pack as packMatching } from '../../models/othello-matching'; -import Game, { pack as packGame } from '../../models/othello-game'; +import OthelloGame, { pack as packGame } from '../../models/othello-game'; import User from '../../models/user'; import publishUserStream, { publishOthelloStream } from '../../event'; import { eighteight } from '../../../common/othello/maps'; module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [childId, childIdErr] = $(params.user_id).id().$; - if (childIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [childId, childIdErr] = $(params.userId).id().$; + if (childIdErr) return rej('invalid userId param'); // Myself if (childId.equals(user._id)) { - return rej('invalid user_id param'); + return rej('invalid userId param'); } // Find session const exist = await Matching.findOne({ - parent_id: childId, - child_id: user._id + parentId: childId, + childId: user._id }); if (exist) { @@ -28,29 +28,29 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Create game - const game = await Game.insert({ - created_at: new Date(), - user1_id: exist.parent_id, - user2_id: user._id, - user1_accepted: false, - user2_accepted: false, - is_started: false, - is_ended: false, + const game = await OthelloGame.insert({ + createdAt: new Date(), + user1Id: exist.parentId, + user2Id: user._id, + user1Accepted: false, + user2Accepted: false, + isStarted: false, + isEnded: false, logs: [], settings: { map: eighteight.data, bw: 'random', - is_llotheo: false + isLlotheo: false } }); // Reponse res(await packGame(game, user)); - publishOthelloStream(exist.parent_id, 'matched', await packGame(game, exist.parent_id)); + publishOthelloStream(exist.parentId, 'matched', await packGame(game, exist.parentId)); const other = await Matching.count({ - child_id: user._id + childId: user._id }); if (other == 0) { @@ -72,14 +72,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // 以前のセッションはすべて削除しておく await Matching.remove({ - parent_id: user._id + parentId: user._id }); // セッションを作成 const matching = await Matching.insert({ - created_at: new Date(), - parent_id: user._id, - child_id: child._id + createdAt: new Date(), + parentId: user._id, + childId: child._id }); // Reponse diff --git a/src/server/api/endpoints/othello/match/cancel.ts b/src/server/api/endpoints/othello/match/cancel.ts index 6f751ef835..ee0f82a611 100644 --- a/src/server/api/endpoints/othello/match/cancel.ts +++ b/src/server/api/endpoints/othello/match/cancel.ts @@ -2,7 +2,7 @@ import Matching from '../../../models/othello-matching'; module.exports = (params, user) => new Promise(async (res, rej) => { await Matching.remove({ - parent_id: user._id + parentId: user._id }); res(); diff --git a/src/server/api/endpoints/posts.ts b/src/server/api/endpoints/posts.ts index 7df744d2a3..bee1de02d4 100644 --- a/src/server/api/endpoints/posts.ts +++ b/src/server/api/endpoints/posts.ts @@ -35,17 +35,17 @@ module.exports = (params) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } // Construct query @@ -65,15 +65,15 @@ module.exports = (params) => new Promise(async (res, rej) => { } if (reply != undefined) { - query.reply_id = reply ? { $exists: true, $ne: null } : null; + query.replyId = reply ? { $exists: true, $ne: null } : null; } if (repost != undefined) { - query.repost_id = repost ? { $exists: true, $ne: null } : null; + query.repostId = repost ? { $exists: true, $ne: null } : null; } if (media != undefined) { - query.media_ids = media ? { $exists: true, $ne: null } : null; + query.mediaIds = media ? { $exists: true, $ne: null } : null; } if (poll != undefined) { @@ -82,7 +82,7 @@ module.exports = (params) => new Promise(async (res, rej) => { // TODO //if (bot != undefined) { - // query.is_bot = bot; + // query.isBot = bot; //} // Issue query diff --git a/src/server/api/endpoints/posts/categorize.ts b/src/server/api/endpoints/posts/categorize.ts index 0c85c2b4e0..0436c8e697 100644 --- a/src/server/api/endpoints/posts/categorize.ts +++ b/src/server/api/endpoints/posts/categorize.ts @@ -12,13 +12,13 @@ import Post from '../../models/post'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - if (!user.account.is_pro) { + if (!user.account.isPro) { return rej('This endpoint is available only from a Pro account'); } - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Get categorizee const post = await Post.findOne({ diff --git a/src/server/api/endpoints/posts/context.ts b/src/server/api/endpoints/posts/context.ts index 5ba3758975..44a77d102e 100644 --- a/src/server/api/endpoints/posts/context.ts +++ b/src/server/api/endpoints/posts/context.ts @@ -12,9 +12,9 @@ import Post, { pack } from '../../models/post'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; @@ -48,13 +48,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => { return; } - if (p.reply_id) { - await get(p.reply_id); + if (p.replyId) { + await get(p.replyId); } } - if (post.reply_id) { - await get(post.reply_id); + if (post.replyId) { + await get(post.replyId); } // Serialize diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts index bc9af843b6..390370ad42 100644 --- a/src/server/api/endpoints/posts/create.ts +++ b/src/server/api/endpoints/posts/create.ts @@ -33,9 +33,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; if (textErr) return rej('invalid text'); - // Get 'via_mobile' parameter - const [viaMobile = false, viaMobileErr] = $(params.via_mobile).optional.boolean().$; - if (viaMobileErr) return rej('invalid via_mobile'); + // Get 'viaMobile' parameter + const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$; + if (viaMobileErr) return rej('invalid viaMobile'); // Get 'tags' parameter const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$; @@ -53,9 +53,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { .$; if (geoErr) return rej('invalid geo'); - // Get 'media_ids' parameter - const [mediaIds, mediaIdsErr] = $(params.media_ids).optional.array('id').unique().range(1, 4).$; - if (mediaIdsErr) return rej('invalid media_ids'); + // Get 'mediaIds' parameter + const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$; + if (mediaIdsErr) return rej('invalid mediaIds'); let files = []; if (mediaIds !== undefined) { @@ -67,7 +67,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // SELECT _id const entity = await DriveFile.findOne({ _id: mediaId, - 'metadata.user_id': user._id + 'metadata.userId': user._id }); if (entity === null) { @@ -80,9 +80,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { files = null; } - // Get 'repost_id' parameter - const [repostId, repostIdErr] = $(params.repost_id).optional.id().$; - if (repostIdErr) return rej('invalid repost_id'); + // Get 'repostId' parameter + const [repostId, repostIdErr] = $(params.repostId).optional.id().$; + if (repostIdErr) return rej('invalid repostId'); let repost: IPost = null; let isQuote = false; @@ -94,13 +94,13 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { if (repost == null) { return rej('repostee is not found'); - } else if (repost.repost_id && !repost.text && !repost.media_ids) { + } else if (repost.repostId && !repost.text && !repost.mediaIds) { return rej('cannot repost to repost'); } // Fetch recently post const latestPost = await Post.findOne({ - user_id: user._id + userId: user._id }, { sort: { _id: -1 @@ -111,8 +111,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // 直近と同じRepost対象かつ引用じゃなかったらエラー if (latestPost && - latestPost.repost_id && - latestPost.repost_id.equals(repost._id) && + latestPost.repostId && + latestPost.repostId.equals(repost._id) && !isQuote) { return rej('cannot repost same post that already reposted in your latest post'); } @@ -125,9 +125,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { } } - // Get 'reply_id' parameter - const [replyId, replyIdErr] = $(params.reply_id).optional.id().$; - if (replyIdErr) return rej('invalid reply_id'); + // Get 'replyId' parameter + const [replyId, replyIdErr] = $(params.replyId).optional.id().$; + if (replyIdErr) return rej('invalid replyId'); let reply: IPost = null; if (replyId !== undefined) { @@ -141,14 +141,14 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { } // 返信対象が引用でないRepostだったらエラー - if (reply.repost_id && !reply.text && !reply.media_ids) { + if (reply.repostId && !reply.text && !reply.mediaIds) { return rej('cannot reply to repost'); } } - // Get 'channel_id' parameter - const [channelId, channelIdErr] = $(params.channel_id).optional.id().$; - if (channelIdErr) return rej('invalid channel_id'); + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).optional.id().$; + if (channelIdErr) return rej('invalid channelId'); let channel: IChannel = null; if (channelId !== undefined) { @@ -162,12 +162,12 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { } // 返信対象の投稿がこのチャンネルじゃなかったらダメ - if (reply && !channelId.equals(reply.channel_id)) { + if (reply && !channelId.equals(reply.channelId)) { return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません'); } // Repost対象の投稿がこのチャンネルじゃなかったらダメ - if (repost && !channelId.equals(repost.channel_id)) { + if (repost && !channelId.equals(repost.channelId)) { return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません'); } @@ -177,12 +177,12 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { } } else { // 返信対象の投稿がチャンネルへの投稿だったらダメ - if (reply && reply.channel_id != null) { + if (reply && reply.channelId != null) { return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません'); } // Repost対象の投稿がチャンネルへの投稿だったらダメ - if (repost && repost.channel_id != null) { + if (repost && repost.channelId != null) { return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません'); } } @@ -206,22 +206,22 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // テキストが無いかつ添付ファイルが無いかつRepostも無いかつ投票も無かったらエラー if (text === undefined && files === null && repost === null && poll === undefined) { - return rej('text, media_ids, repost_id or poll is required'); + return rej('text, mediaIds, repostId or poll is required'); } // 直近の投稿と重複してたらエラー // TODO: 直近の投稿が一日前くらいなら重複とは見なさない - if (user.latest_post) { + if (user.latestPost) { if (deepEqual({ - text: user.latest_post.text, - reply: user.latest_post.reply_id ? user.latest_post.reply_id.toString() : null, - repost: user.latest_post.repost_id ? user.latest_post.repost_id.toString() : null, - media_ids: (user.latest_post.media_ids || []).map(id => id.toString()) + text: user.latestPost.text, + reply: user.latestPost.replyId ? user.latestPost.replyId.toString() : null, + repost: user.latestPost.repostId ? user.latestPost.repostId.toString() : null, + mediaIds: (user.latestPost.mediaIds || []).map(id => id.toString()) }, { text: text, reply: reply ? reply._id.toString() : null, repost: repost ? repost._id.toString() : null, - media_ids: (files || []).map(file => file._id.toString()) + mediaIds: (files || []).map(file => file._id.toString()) })) { return rej('duplicate'); } @@ -246,23 +246,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // 投稿を作成 const post = await Post.insert({ - created_at: new Date(), - channel_id: channel ? channel._id : undefined, + createdAt: new Date(), + channelId: channel ? channel._id : undefined, index: channel ? channel.index + 1 : undefined, - media_ids: files ? files.map(file => file._id) : undefined, - reply_id: reply ? reply._id : undefined, - repost_id: repost ? repost._id : undefined, + mediaIds: files ? files.map(file => file._id) : undefined, + replyId: reply ? reply._id : undefined, + repostId: repost ? repost._id : undefined, poll: poll, text: text, tags: tags, - user_id: user._id, - app_id: app ? app._id : null, - via_mobile: viaMobile, + userId: user._id, + appId: app ? app._id : null, + viaMobile: viaMobile, geo, // 以下非正規化データ - _reply: reply ? { user_id: reply.user_id } : undefined, - _repost: repost ? { user_id: repost.user_id } : undefined, + _reply: reply ? { userId: reply.userId } : undefined, + _repost: repost ? { userId: repost.userId } : undefined, }); // Serialize @@ -270,14 +270,14 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // Reponse res({ - created_post: postObj + createdPost: postObj }); //#region Post processes User.update({ _id: user._id }, { $set: { - latest_post: post + latestPost: post } }); @@ -293,10 +293,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // Publish event if (!user._id.equals(mentionee)) { const mentioneeMutes = await Mute.find({ - muter_id: mentionee, - deleted_at: { $exists: false } + muterId: mentionee, + deletedAt: { $exists: false } }); - const mentioneesMutedUserIds = mentioneeMutes.map(m => m.mutee_id.toString()); + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) { event(mentionee, reason, postObj); pushSw(mentionee, reason, postObj); @@ -312,17 +312,17 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // Fetch all followers const followers = await Following .find({ - followee_id: user._id, + followeeId: user._id, // 削除されたドキュメントは除く - deleted_at: { $exists: false } + deletedAt: { $exists: false } }, { - follower_id: true, + followerId: true, _id: false }); // Publish event to followers stream followers.forEach(following => - event(following.follower_id, 'post', postObj)); + event(following.followerId, 'post', postObj)); } // チャンネルへの投稿 @@ -339,21 +339,21 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // Get channel watchers const watches = await ChannelWatching.find({ - channel_id: channel._id, + channelId: channel._id, // 削除されたドキュメントは除く - deleted_at: { $exists: false } + deletedAt: { $exists: false } }); // チャンネルの視聴者(のタイムライン)に配信 watches.forEach(w => { - event(w.user_id, 'post', postObj); + event(w.userId, 'post', postObj); }); } // Increment my posts count User.update({ _id: user._id }, { $inc: { - posts_count: 1 + postsCount: 1 } }); @@ -362,68 +362,68 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // Increment replies count Post.update({ _id: reply._id }, { $inc: { - replies_count: 1 + repliesCount: 1 } }); // 自分自身へのリプライでない限りは通知を作成 - notify(reply.user_id, user._id, 'reply', { - post_id: post._id + notify(reply.userId, user._id, 'reply', { + postId: post._id }); // Fetch watchers Watching .find({ - post_id: reply._id, - user_id: { $ne: user._id }, + postId: reply._id, + userId: { $ne: user._id }, // 削除されたドキュメントは除く - deleted_at: { $exists: false } + deletedAt: { $exists: false } }, { fields: { - user_id: true + userId: true } }) .then(watchers => { watchers.forEach(watcher => { - notify(watcher.user_id, user._id, 'reply', { - post_id: post._id + notify(watcher.userId, user._id, 'reply', { + postId: post._id }); }); }); // この投稿をWatchする - if ((user.account as ILocalAccount).settings.auto_watch !== false) { + if ((user.account as ILocalAccount).settings.autoWatch !== false) { watch(user._id, reply); } // Add mention - addMention(reply.user_id, 'reply'); + addMention(reply.userId, 'reply'); } // If it is repost if (repost) { // Notify const type = text ? 'quote' : 'repost'; - notify(repost.user_id, user._id, type, { - post_id: post._id + notify(repost.userId, user._id, type, { + postId: post._id }); // Fetch watchers Watching .find({ - post_id: repost._id, - user_id: { $ne: user._id }, + postId: repost._id, + userId: { $ne: user._id }, // 削除されたドキュメントは除く - deleted_at: { $exists: false } + deletedAt: { $exists: false } }, { fields: { - user_id: true + userId: true } }) .then(watchers => { watchers.forEach(watcher => { - notify(watcher.user_id, user._id, type, { - post_id: post._id + notify(watcher.userId, user._id, type, { + postId: post._id }); }); }); @@ -436,18 +436,18 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // If it is quote repost if (text) { // Add mention - addMention(repost.user_id, 'quote'); + addMention(repost.userId, 'quote'); } else { // Publish event - if (!user._id.equals(repost.user_id)) { - event(repost.user_id, 'repost', postObj); + if (!user._id.equals(repost.userId)) { + event(repost.userId, 'repost', postObj); } } // 今までで同じ投稿をRepostしているか const existRepost = await Post.findOne({ - user_id: user._id, - repost_id: repost._id, + userId: user._id, + repostId: repost._id, _id: { $ne: post._id } @@ -457,7 +457,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // Update repostee status Post.update({ _id: repost._id }, { $inc: { - repost_count: 1 + repostCount: 1 } }); } @@ -494,15 +494,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { if (mentionee == null) return; // 既に言及されたユーザーに対する返信や引用repostの場合も無視 - if (reply && reply.user_id.equals(mentionee._id)) return; - if (repost && repost.user_id.equals(mentionee._id)) return; + if (reply && reply.userId.equals(mentionee._id)) return; + if (repost && repost.userId.equals(mentionee._id)) return; // Add mention addMention(mentionee._id, 'mention'); // Create notification notify(mentionee._id, user._id, 'mention', { - post_id: post._id + postId: post._id }); return; diff --git a/src/server/api/endpoints/posts/favorites/create.ts b/src/server/api/endpoints/posts/favorites/create.ts index f9dee271b5..6100e10b23 100644 --- a/src/server/api/endpoints/posts/favorites/create.ts +++ b/src/server/api/endpoints/posts/favorites/create.ts @@ -13,9 +13,9 @@ import Post from '../../../models/post'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Get favoritee const post = await Post.findOne({ @@ -28,8 +28,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // if already favorited const exist = await Favorite.findOne({ - post_id: post._id, - user_id: user._id + postId: post._id, + userId: user._id }); if (exist !== null) { @@ -38,9 +38,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Create favorite await Favorite.insert({ - created_at: new Date(), - post_id: post._id, - user_id: user._id + createdAt: new Date(), + postId: post._id, + userId: user._id }); // Send response diff --git a/src/server/api/endpoints/posts/favorites/delete.ts b/src/server/api/endpoints/posts/favorites/delete.ts index c4fe7d3234..db52036ec5 100644 --- a/src/server/api/endpoints/posts/favorites/delete.ts +++ b/src/server/api/endpoints/posts/favorites/delete.ts @@ -13,9 +13,9 @@ import Post from '../../../models/post'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Get favoritee const post = await Post.findOne({ @@ -28,8 +28,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // if already favorited const exist = await Favorite.findOne({ - post_id: post._id, - user_id: user._id + postId: post._id, + userId: user._id }); if (exist === null) { @@ -37,7 +37,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Delete favorite - await Favorite.deleteOne({ + await Favorite.remove({ _id: exist._id }); diff --git a/src/server/api/endpoints/posts/mentions.ts b/src/server/api/endpoints/posts/mentions.ts index 7127db0ad1..1b342e8de9 100644 --- a/src/server/api/endpoints/posts/mentions.ts +++ b/src/server/api/endpoints/posts/mentions.ts @@ -23,17 +23,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } // Construct query @@ -48,7 +48,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (following) { const followingIds = await getFriends(user._id); - query.user_id = { + query.userId = { $in: followingIds }; } diff --git a/src/server/api/endpoints/posts/polls/recommendation.ts b/src/server/api/endpoints/posts/polls/recommendation.ts index 4a3fa3f55e..19ef0975fa 100644 --- a/src/server/api/endpoints/posts/polls/recommendation.ts +++ b/src/server/api/endpoints/posts/polls/recommendation.ts @@ -23,22 +23,22 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Get votes const votes = await Vote.find({ - user_id: user._id + userId: user._id }, { fields: { _id: false, - post_id: true + postId: true } }); - const nin = votes && votes.length != 0 ? votes.map(v => v.post_id) : []; + const nin = votes && votes.length != 0 ? votes.map(v => v.postId) : []; const posts = await Post .find({ _id: { $nin: nin }, - user_id: { + userId: { $ne: user._id }, poll: { diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts index 16ce76a6fa..734a3a3c45 100644 --- a/src/server/api/endpoints/posts/polls/vote.ts +++ b/src/server/api/endpoints/posts/polls/vote.ts @@ -17,9 +17,9 @@ import { publishPostStream } from '../../../event'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Get votee const post = await Post.findOne({ @@ -43,8 +43,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // if already voted const exist = await Vote.findOne({ - post_id: post._id, - user_id: user._id + postId: post._id, + userId: user._id }); if (exist !== null) { @@ -53,9 +53,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Create vote await Vote.insert({ - created_at: new Date(), - post_id: post._id, - user_id: user._id, + createdAt: new Date(), + postId: post._id, + userId: user._id, choice: choice }); @@ -73,34 +73,34 @@ module.exports = (params, user) => new Promise(async (res, rej) => { publishPostStream(post._id, 'poll_voted'); // Notify - notify(post.user_id, user._id, 'poll_vote', { - post_id: post._id, + notify(post.userId, user._id, 'poll_vote', { + postId: post._id, choice: choice }); // Fetch watchers Watching .find({ - post_id: post._id, - user_id: { $ne: user._id }, + postId: post._id, + userId: { $ne: user._id }, // 削除されたドキュメントは除く - deleted_at: { $exists: false } + deletedAt: { $exists: false } }, { fields: { - user_id: true + userId: true } }) .then(watchers => { watchers.forEach(watcher => { - notify(watcher.user_id, user._id, 'poll_vote', { - post_id: post._id, + notify(watcher.userId, user._id, 'poll_vote', { + postId: post._id, choice: choice }); }); }); // この投稿をWatchする - if (user.account.settings.auto_watch !== false) { + if (user.account.settings.autoWatch !== false) { watch(user._id, post); } }); diff --git a/src/server/api/endpoints/posts/reactions.ts b/src/server/api/endpoints/posts/reactions.ts index feb140ab41..f753ba7c29 100644 --- a/src/server/api/endpoints/posts/reactions.ts +++ b/src/server/api/endpoints/posts/reactions.ts @@ -13,9 +13,9 @@ import Reaction, { pack } from '../../models/post-reaction'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; @@ -41,8 +41,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Issue query const reactions = await Reaction .find({ - post_id: post._id, - deleted_at: { $exists: false } + postId: post._id, + deletedAt: { $exists: false } }, { limit: limit, skip: offset, diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts index f77afed40c..a1e6779805 100644 --- a/src/server/api/endpoints/posts/reactions/create.ts +++ b/src/server/api/endpoints/posts/reactions/create.ts @@ -18,9 +18,9 @@ import { publishPostStream, pushSw } from '../../../event'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Get 'reaction' parameter const [reaction, reactionErr] = $(params.reaction).string().or([ @@ -46,15 +46,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Myself - if (post.user_id.equals(user._id)) { + if (post.userId.equals(user._id)) { return rej('cannot react to my post'); } // if already reacted const exist = await Reaction.findOne({ - post_id: post._id, - user_id: user._id, - deleted_at: { $exists: false } + postId: post._id, + userId: user._id, + deletedAt: { $exists: false } }); if (exist !== null) { @@ -63,9 +63,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Create reaction await Reaction.insert({ - created_at: new Date(), - post_id: post._id, - user_id: user._id, + createdAt: new Date(), + postId: post._id, + userId: user._id, reaction: reaction }); @@ -73,7 +73,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { res(); const inc = {}; - inc[`reaction_counts.${reaction}`] = 1; + inc[`reactionCounts.${reaction}`] = 1; // Increment reactions count await Post.update({ _id: post._id }, { @@ -83,40 +83,40 @@ module.exports = (params, user) => new Promise(async (res, rej) => { publishPostStream(post._id, 'reacted'); // Notify - notify(post.user_id, user._id, 'reaction', { - post_id: post._id, + notify(post.userId, user._id, 'reaction', { + postId: post._id, reaction: reaction }); - pushSw(post.user_id, 'reaction', { - user: await packUser(user, post.user_id), - post: await packPost(post, post.user_id), + pushSw(post.userId, 'reaction', { + user: await packUser(user, post.userId), + post: await packPost(post, post.userId), reaction: reaction }); // Fetch watchers Watching .find({ - post_id: post._id, - user_id: { $ne: user._id }, + postId: post._id, + userId: { $ne: user._id }, // 削除されたドキュメントは除く - deleted_at: { $exists: false } + deletedAt: { $exists: false } }, { fields: { - user_id: true + userId: true } }) .then(watchers => { watchers.forEach(watcher => { - notify(watcher.user_id, user._id, 'reaction', { - post_id: post._id, + notify(watcher.userId, user._id, 'reaction', { + postId: post._id, reaction: reaction }); }); }); // この投稿をWatchする - if (user.account.settings.auto_watch !== false) { + if (user.account.settings.autoWatch !== false) { watch(user._id, post); } }); diff --git a/src/server/api/endpoints/posts/reactions/delete.ts b/src/server/api/endpoints/posts/reactions/delete.ts index 922c57ab18..b09bcbb4b7 100644 --- a/src/server/api/endpoints/posts/reactions/delete.ts +++ b/src/server/api/endpoints/posts/reactions/delete.ts @@ -14,9 +14,9 @@ import Post from '../../../models/post'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Fetch unreactee const post = await Post.findOne({ @@ -29,9 +29,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // if already unreacted const exist = await Reaction.findOne({ - post_id: post._id, - user_id: user._id, - deleted_at: { $exists: false } + postId: post._id, + userId: user._id, + deletedAt: { $exists: false } }); if (exist === null) { @@ -43,7 +43,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: exist._id }, { $set: { - deleted_at: new Date() + deletedAt: new Date() } }); @@ -51,7 +51,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { res(); const dec = {}; - dec[`reaction_counts.${exist.reaction}`] = -1; + dec[`reactionCounts.${exist.reaction}`] = -1; // Decrement reactions count Post.update({ _id: post._id }, { diff --git a/src/server/api/endpoints/posts/replies.ts b/src/server/api/endpoints/posts/replies.ts index 613c4fa24c..db021505fc 100644 --- a/src/server/api/endpoints/posts/replies.ts +++ b/src/server/api/endpoints/posts/replies.ts @@ -12,9 +12,9 @@ import Post, { pack } from '../../models/post'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; @@ -39,7 +39,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Issue query const replies = await Post - .find({ reply_id: post._id }, { + .find({ replyId: post._id }, { limit: limit, skip: offset, sort: { diff --git a/src/server/api/endpoints/posts/reposts.ts b/src/server/api/endpoints/posts/reposts.ts index 89ab0e3d55..51af41f523 100644 --- a/src/server/api/endpoints/posts/reposts.ts +++ b/src/server/api/endpoints/posts/reposts.ts @@ -12,25 +12,25 @@ import Post, { pack } from '../../models/post'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and until_id is specified + // Check if both of sinceId and untilId is specified if (sinceId && untilId) { - return rej('cannot set since_id and until_id'); + return rej('cannot set sinceId and untilId'); } // Lookup post @@ -47,7 +47,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: -1 }; const query = { - repost_id: post._id + repostId: post._id } as any; if (sinceId) { sort._id = 1; diff --git a/src/server/api/endpoints/posts/search.ts b/src/server/api/endpoints/posts/search.ts index a36d1178a5..bb5c438926 100644 --- a/src/server/api/endpoints/posts/search.ts +++ b/src/server/api/endpoints/posts/search.ts @@ -21,21 +21,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const [text, textError] = $(params.text).optional.string().$; if (textError) return rej('invalid text param'); - // Get 'include_user_ids' parameter - const [includeUserIds = [], includeUserIdsErr] = $(params.include_user_ids).optional.array('id').$; - if (includeUserIdsErr) return rej('invalid include_user_ids param'); + // Get 'includeUserIds' parameter + const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array('id').$; + if (includeUserIdsErr) return rej('invalid includeUserIds param'); - // Get 'exclude_user_ids' parameter - const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_user_ids).optional.array('id').$; - if (excludeUserIdsErr) return rej('invalid exclude_user_ids param'); + // Get 'excludeUserIds' parameter + const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array('id').$; + if (excludeUserIdsErr) return rej('invalid excludeUserIds param'); - // Get 'include_user_usernames' parameter - const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.include_user_usernames).optional.array('string').$; - if (includeUserUsernamesErr) return rej('invalid include_user_usernames param'); + // Get 'includeUserUsernames' parameter + const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array('string').$; + if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param'); - // Get 'exclude_user_usernames' parameter - const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.exclude_user_usernames).optional.array('string').$; - if (excludeUserUsernamesErr) return rej('invalid exclude_user_usernames param'); + // Get 'excludeUserUsernames' parameter + const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array('string').$; + if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param'); // Get 'following' parameter const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$; @@ -61,13 +61,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$; if (pollErr) return rej('invalid poll param'); - // Get 'since_date' parameter - const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$; - if (sinceDateErr) throw 'invalid since_date param'; + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; - // Get 'until_date' parameter - const [untilDate, untilDateErr] = $(params.until_date).optional.number().$; - if (untilDateErr) throw 'invalid until_date param'; + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + if (untilDateErr) throw 'invalid untilDate param'; // Get 'offset' parameter const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; @@ -81,7 +81,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (includeUserUsernames != null) { const ids = (await Promise.all(includeUserUsernames.map(async (username) => { const _user = await User.findOne({ - username_lower: username.toLowerCase() + usernameLower: username.toLowerCase() }); return _user ? _user._id : null; }))).filter(id => id != null); @@ -92,7 +92,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (excludeUserUsernames != null) { const ids = (await Promise.all(excludeUserUsernames.map(async (username) => { const _user = await User.findOne({ - username_lower: username.toLowerCase() + usernameLower: username.toLowerCase() }); return _user ? _user._id : null; }))).filter(id => id != null); @@ -143,13 +143,13 @@ async function search( if (includeUserIds && includeUserIds.length != 0) { push({ - user_id: { + userId: { $in: includeUserIds } }); } else if (excludeUserIds && excludeUserIds.length != 0) { push({ - user_id: { + userId: { $nin: excludeUserIds } }); @@ -158,7 +158,7 @@ async function search( if (following != null && me != null) { const ids = await getFriends(me._id, false); push({ - user_id: following ? { + userId: following ? { $in: ids } : { $nin: ids.concat(me._id) @@ -168,45 +168,45 @@ async function search( if (me != null) { const mutes = await Mute.find({ - muter_id: me._id, - deleted_at: { $exists: false } + muterId: me._id, + deletedAt: { $exists: false } }); - const mutedUserIds = mutes.map(m => m.mutee_id); + const mutedUserIds = mutes.map(m => m.muteeId); switch (mute) { case 'mute_all': push({ - user_id: { + userId: { $nin: mutedUserIds }, - '_reply.user_id': { + '_reply.userId': { $nin: mutedUserIds }, - '_repost.user_id': { + '_repost.userId': { $nin: mutedUserIds } }); break; case 'mute_related': push({ - '_reply.user_id': { + '_reply.userId': { $nin: mutedUserIds }, - '_repost.user_id': { + '_repost.userId': { $nin: mutedUserIds } }); break; case 'mute_direct': push({ - user_id: { + userId: { $nin: mutedUserIds } }); break; case 'direct_only': push({ - user_id: { + userId: { $in: mutedUserIds } }); @@ -214,11 +214,11 @@ async function search( case 'related_only': push({ $or: [{ - '_reply.user_id': { + '_reply.userId': { $in: mutedUserIds } }, { - '_repost.user_id': { + '_repost.userId': { $in: mutedUserIds } }] @@ -227,15 +227,15 @@ async function search( case 'all_only': push({ $or: [{ - user_id: { + userId: { $in: mutedUserIds } }, { - '_reply.user_id': { + '_reply.userId': { $in: mutedUserIds } }, { - '_repost.user_id': { + '_repost.userId': { $in: mutedUserIds } }] @@ -247,7 +247,7 @@ async function search( if (reply != null) { if (reply) { push({ - reply_id: { + replyId: { $exists: true, $ne: null } @@ -255,11 +255,11 @@ async function search( } else { push({ $or: [{ - reply_id: { + replyId: { $exists: false } }, { - reply_id: null + replyId: null }] }); } @@ -268,7 +268,7 @@ async function search( if (repost != null) { if (repost) { push({ - repost_id: { + repostId: { $exists: true, $ne: null } @@ -276,11 +276,11 @@ async function search( } else { push({ $or: [{ - repost_id: { + repostId: { $exists: false } }, { - repost_id: null + repostId: null }] }); } @@ -289,7 +289,7 @@ async function search( if (media != null) { if (media) { push({ - media_ids: { + mediaIds: { $exists: true, $ne: null } @@ -297,11 +297,11 @@ async function search( } else { push({ $or: [{ - media_ids: { + mediaIds: { $exists: false } }, { - media_ids: null + mediaIds: null }] }); } @@ -330,7 +330,7 @@ async function search( if (sinceDate) { push({ - created_at: { + createdAt: { $gt: new Date(sinceDate) } }); @@ -338,7 +338,7 @@ async function search( if (untilDate) { push({ - created_at: { + createdAt: { $lt: new Date(untilDate) } }); diff --git a/src/server/api/endpoints/posts/show.ts b/src/server/api/endpoints/posts/show.ts index 3839490597..bb4bcdb790 100644 --- a/src/server/api/endpoints/posts/show.ts +++ b/src/server/api/endpoints/posts/show.ts @@ -12,9 +12,9 @@ import Post, { pack } from '../../models/post'; * @return {Promise} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'postId' parameter + const [postId, postIdErr] = $(params.postId).id().$; + if (postIdErr) return rej('invalid postId param'); // Get post const post = await Post.findOne({ diff --git a/src/server/api/endpoints/posts/timeline.ts b/src/server/api/endpoints/posts/timeline.ts index c41cfdb8bd..a3e915f16a 100644 --- a/src/server/api/endpoints/posts/timeline.ts +++ b/src/server/api/endpoints/posts/timeline.ts @@ -22,25 +22,25 @@ module.exports = async (params, user, app) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) throw 'invalid limit param'; - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) throw 'invalid since_id param'; + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) throw 'invalid sinceId param'; - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) throw 'invalid until_id param'; + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) throw 'invalid untilId param'; - // Get 'since_date' parameter - const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$; - if (sinceDateErr) throw 'invalid since_date param'; + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; - // Get 'until_date' parameter - const [untilDate, untilDateErr] = $(params.until_date).optional.number().$; - if (untilDateErr) throw 'invalid until_date param'; + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + if (untilDateErr) throw 'invalid untilDate param'; - // Check if only one of since_id, until_id, since_date, until_date specified + // Check if only one of sinceId, untilId, sinceDate, untilDate specified if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { - throw 'only one of since_id, until_id, since_date, until_date can be specified'; + throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } const { followingIds, watchingChannelIds, mutedUserIds } = await rap({ @@ -49,17 +49,17 @@ module.exports = async (params, user, app) => { // Watchしているチャンネルを取得 watchingChannelIds: ChannelWatching.find({ - user_id: user._id, + userId: user._id, // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }).then(watches => watches.map(w => w.channel_id)), + deletedAt: { $exists: false } + }).then(watches => watches.map(w => w.channelId)), // ミュートしているユーザーを取得 mutedUserIds: Mute.find({ - muter_id: user._id, + muterId: user._id, // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }).then(ms => ms.map(m => m.mutee_id)) + deletedAt: { $exists: false } + }).then(ms => ms.map(m => m.muteeId)) }); //#region Construct query @@ -70,31 +70,31 @@ module.exports = async (params, user, app) => { const query = { $or: [{ // フォローしている人のタイムラインへの投稿 - user_id: { + userId: { $in: followingIds }, // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る $or: [{ - channel_id: { + channelId: { $exists: false } }, { - channel_id: null + channelId: null }] }, { // Watchしているチャンネルへの投稿 - channel_id: { + channelId: { $in: watchingChannelIds } }], // mute - user_id: { + userId: { $nin: mutedUserIds }, - '_reply.user_id': { + '_reply.userId': { $nin: mutedUserIds }, - '_repost.user_id': { + '_repost.userId': { $nin: mutedUserIds }, } as any; @@ -110,11 +110,11 @@ module.exports = async (params, user, app) => { }; } else if (sinceDate) { sort._id = 1; - query.created_at = { + query.createdAt = { $gt: new Date(sinceDate) }; } else if (untilDate) { - query.created_at = { + query.createdAt = { $lt: new Date(untilDate) }; } diff --git a/src/server/api/endpoints/posts/trend.ts b/src/server/api/endpoints/posts/trend.ts index caded92bf5..bc0c47fbc6 100644 --- a/src/server/api/endpoints/posts/trend.ts +++ b/src/server/api/endpoints/posts/trend.ts @@ -38,24 +38,24 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (pollErr) return rej('invalid poll param'); const query = { - created_at: { + createdAt: { $gte: new Date(Date.now() - ms('1days')) }, - repost_count: { + repostCount: { $gt: 0 } } as any; if (reply != undefined) { - query.reply_id = reply ? { $exists: true, $ne: null } : null; + query.replyId = reply ? { $exists: true, $ne: null } : null; } if (repost != undefined) { - query.repost_id = repost ? { $exists: true, $ne: null } : null; + query.repostId = repost ? { $exists: true, $ne: null } : null; } if (media != undefined) { - query.media_ids = media ? { $exists: true, $ne: null } : null; + query.mediaIds = media ? { $exists: true, $ne: null } : null; } if (poll != undefined) { @@ -68,7 +68,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { limit: limit, skip: offset, sort: { - repost_count: -1, + repostCount: -1, _id: -1 } }); diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts index a6084cd17a..719792d40d 100644 --- a/src/server/api/endpoints/stats.ts +++ b/src/server/api/endpoints/stats.ts @@ -15,10 +15,10 @@ import User from '../models/user'; * schema: * type: object * properties: - * posts_count: + * postsCount: * description: count of all posts of misskey * type: number - * users_count: + * usersCount: * description: count of all users of misskey * type: number * @@ -42,7 +42,7 @@ module.exports = params => new Promise(async (res, rej) => { .count(); res({ - posts_count: postsCount, - users_count: usersCount + postsCount: postsCount, + usersCount: usersCount }); }); diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts index 99406138db..1542e1dbeb 100644 --- a/src/server/api/endpoints/sw/register.ts +++ b/src/server/api/endpoints/sw/register.ts @@ -28,11 +28,11 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re // if already subscribed const exist = await Subscription.findOne({ - user_id: user._id, + userId: user._id, endpoint: endpoint, auth: auth, publickey: publickey, - deleted_at: { $exists: false } + deletedAt: { $exists: false } }); if (exist !== null) { @@ -40,7 +40,7 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re } await Subscription.insert({ - user_id: user._id, + userId: user._id, endpoint: endpoint, auth: auth, publickey: publickey diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts index aac7fadf5a..f23cdbd85a 100644 --- a/src/server/api/endpoints/username/available.ts +++ b/src/server/api/endpoints/username/available.ts @@ -20,7 +20,7 @@ module.exports = async (params) => new Promise(async (res, rej) => { const exist = await User .count({ host: null, - username_lower: username.toLowerCase() + usernameLower: username.toLowerCase() }, { limit: 1 }); diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts index 4acc13c281..393c3479c5 100644 --- a/src/server/api/endpoints/users.ts +++ b/src/server/api/endpoints/users.ts @@ -29,11 +29,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (sort) { if (sort == '+follower') { _sort = { - followers_count: -1 + followersCount: -1 }; } else if (sort == '-follower') { _sort = { - followers_count: 1 + followersCount: 1 }; } } else { diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts index b0fb83c683..fc09cfa2c6 100644 --- a/src/server/api/endpoints/users/followers.ts +++ b/src/server/api/endpoints/users/followers.ts @@ -15,9 +15,9 @@ import getFriends from '../../common/get-friends'; * @return {Promise} */ module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Get 'iknow' parameter const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$; @@ -46,8 +46,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Construct query const query = { - followee_id: user._id, - deleted_at: { $exists: false } + followeeId: user._id, + deletedAt: { $exists: false } } as any; // ログインしていてかつ iknow フラグがあるとき @@ -55,7 +55,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Get my friends const myFriends = await getFriends(me._id); - query.follower_id = { + query.followerId = { $in: myFriends }; } @@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize const users = await Promise.all(following.map(async f => - await pack(f.follower_id, me, { detail: true }))); + await pack(f.followerId, me, { detail: true }))); // Response res({ diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts index 8e88431e92..3387dab366 100644 --- a/src/server/api/endpoints/users/following.ts +++ b/src/server/api/endpoints/users/following.ts @@ -15,9 +15,9 @@ import getFriends from '../../common/get-friends'; * @return {Promise} */ module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Get 'iknow' parameter const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$; @@ -46,8 +46,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Construct query const query = { - follower_id: user._id, - deleted_at: { $exists: false } + followerId: user._id, + deletedAt: { $exists: false } } as any; // ログインしていてかつ iknow フラグがあるとき @@ -55,7 +55,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Get my friends const myFriends = await getFriends(me._id); - query.followee_id = { + query.followeeId = { $in: myFriends }; } @@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize const users = await Promise.all(following.map(async f => - await pack(f.followee_id, me, { detail: true }))); + await pack(f.followeeId, me, { detail: true }))); // Response res({ diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts index 87f4f77a5b..991c5555b6 100644 --- a/src/server/api/endpoints/users/get_frequently_replied_users.ts +++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts @@ -6,9 +6,9 @@ import Post from '../../models/post'; import User, { pack } from '../../models/user'; module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; @@ -29,8 +29,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Fetch recent posts const recentPosts = await Post.find({ - user_id: user._id, - reply_id: { + userId: user._id, + replyId: { $exists: true, $ne: null } @@ -41,7 +41,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { limit: 1000, fields: { _id: false, - reply_id: true + replyId: true } }); @@ -52,15 +52,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const replyTargetPosts = await Post.find({ _id: { - $in: recentPosts.map(p => p.reply_id) + $in: recentPosts.map(p => p.replyId) }, - user_id: { + userId: { $ne: user._id } }, { fields: { _id: false, - user_id: true + userId: true } }); @@ -68,7 +68,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Extract replies from recent posts replyTargetPosts.forEach(post => { - const userId = post.user_id.toString(); + const userId = post.userId.toString(); if (repliedUsers[userId]) { repliedUsers[userId]++; } else { diff --git a/src/server/api/endpoints/users/posts.ts b/src/server/api/endpoints/users/posts.ts index 3c84bf0d80..9346907492 100644 --- a/src/server/api/endpoints/users/posts.ts +++ b/src/server/api/endpoints/users/posts.ts @@ -14,16 +14,16 @@ import User from '../../models/user'; * @return {Promise} */ module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).optional.id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).optional.id().$; + if (userIdErr) return rej('invalid userId param'); // Get 'username' parameter const [username, usernameErr] = $(params.username).optional.string().$; if (usernameErr) return rej('invalid username param'); if (userId === undefined && username === undefined) { - return rej('user_id or pair of username and host is required'); + return rej('userId or pair of username and host is required'); } // Get 'host' parameter @@ -31,45 +31,45 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (hostErr) return rej('invalid host param'); if (userId === undefined && host === undefined) { - return rej('user_id or pair of username and host is required'); + return rej('userId or pair of username and host is required'); } - // Get 'include_replies' parameter - const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$; - if (includeRepliesErr) return rej('invalid include_replies param'); + // Get 'includeReplies' parameter + const [includeReplies = true, includeRepliesErr] = $(params.includeReplies).optional.boolean().$; + if (includeRepliesErr) return rej('invalid includeReplies param'); - // Get 'with_media' parameter - const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$; - if (withMediaErr) return rej('invalid with_media param'); + // Get 'withMedia' parameter + const [withMedia = false, withMediaErr] = $(params.withMedia).optional.boolean().$; + if (withMediaErr) return rej('invalid withMedia param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'until_id' parameter - const [untilId, untilIdErr] = $(params.until_id).optional.id().$; - if (untilIdErr) return rej('invalid until_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Get 'since_date' parameter - const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$; - if (sinceDateErr) throw 'invalid since_date param'; + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; - // Get 'until_date' parameter - const [untilDate, untilDateErr] = $(params.until_date).optional.number().$; - if (untilDateErr) throw 'invalid until_date param'; + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + if (untilDateErr) throw 'invalid untilDate param'; - // Check if only one of since_id, until_id, since_date, until_date specified + // Check if only one of sinceId, untilId, sinceDate, untilDate specified if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { - throw 'only one of since_id, until_id, since_date, until_date can be specified'; + throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } const q = userId !== undefined ? { _id: userId } - : { username_lower: username.toLowerCase(), host_lower: getHostLower(host) } ; + : { usernameLower: username.toLowerCase(), hostLower: getHostLower(host) } ; // Lookup user const user = await User.findOne(q, { @@ -88,7 +88,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { }; const query = { - user_id: user._id + userId: user._id } as any; if (sinceId) { @@ -102,21 +102,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => { }; } else if (sinceDate) { sort._id = 1; - query.created_at = { + query.createdAt = { $gt: new Date(sinceDate) }; } else if (untilDate) { - query.created_at = { + query.createdAt = { $lt: new Date(untilDate) }; } if (!includeReplies) { - query.reply_id = null; + query.replyId = null; } if (withMedia) { - query.media_ids = { + query.mediaIds = { $exists: true, $ne: null }; diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index 45d90f422b..c5297cdc50 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.last_used_at': { + 'account.lastUsedAt': { $gte: new Date(Date.now() - ms('7days')) } }, { @@ -43,7 +43,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { limit: limit, skip: offset, sort: { - followers_count: -1 + followersCount: -1 } }); diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts index 3c81576440..b03ed2f2fd 100644 --- a/src/server/api/endpoints/users/search.ts +++ b/src/server/api/endpoints/users/search.ts @@ -41,7 +41,7 @@ async function byNative(res, rej, me, query, offset, max) { const users = await User .find({ $or: [{ - username_lower: new RegExp(escapedQuery.replace('@', '').toLowerCase()) + usernameLower: new RegExp(escapedQuery.replace('@', '').toLowerCase()) }, { name: new RegExp(escapedQuery) }] diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts index 9c5e1905aa..24e9c98e78 100644 --- a/src/server/api/endpoints/users/search_by_username.ts +++ b/src/server/api/endpoints/users/search_by_username.ts @@ -26,7 +26,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const users = await User .find({ - username_lower: new RegExp(query.toLowerCase()) + usernameLower: new RegExp(query.toLowerCase()) }, { limit: limit, skip: offset diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts index 78df23f339..16411dddc6 100644 --- a/src/server/api/endpoints/users/show.ts +++ b/src/server/api/endpoints/users/show.ts @@ -56,9 +56,9 @@ function webFingerAndVerify(query, verifier) { module.exports = (params, me) => new Promise(async (res, rej) => { let user; - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).optional.id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).optional.id().$; + if (userIdErr) return rej('invalid userId param'); // Get 'username' parameter const [username, usernameErr] = $(params.username).optional.string().$; @@ -69,25 +69,25 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (hostErr) return rej('invalid username param'); if (userId === undefined && typeof username !== 'string') { - return rej('user_id or pair of username and host is required'); + return rej('userId or pair of username and host is required'); } // Lookup user if (typeof host === 'string') { - const username_lower = username.toLowerCase(); - const host_lower_ascii = toASCII(host).toLowerCase(); - const host_lower = toUnicode(host_lower_ascii); + const usernameLower = username.toLowerCase(); + const hostLower_ascii = toASCII(host).toLowerCase(); + const hostLower = toUnicode(hostLower_ascii); - user = await findUser({ username_lower, host_lower }); + user = await findUser({ usernameLower, hostLower }); if (user === null) { - const acct_lower = `${username_lower}@${host_lower_ascii}`; + const acct_lower = `${usernameLower}@${hostLower_ascii}`; let activityStreams; let finger; - let followers_count; - let following_count; + let followersCount; + let followingCount; let likes_count; - let posts_count; + let postsCount; if (!validateUsername(username)) { return rej('username validation failed'); @@ -122,7 +122,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { activityStreams['@context'] === 'https://www.w3.org/ns/activitystreams') && activityStreams.type === 'Person' && typeof activityStreams.preferredUsername === 'string' && - activityStreams.preferredUsername.toLowerCase() === username_lower && + activityStreams.preferredUsername.toLowerCase() === usernameLower && isValidName(activityStreams.name) && isValidDescription(activityStreams.summary) )) { @@ -130,7 +130,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { } try { - [followers_count, following_count, likes_count, posts_count] = await Promise.all([ + [followersCount, followingCount, likes_count, postsCount] = await Promise.all([ getCollectionCount(activityStreams.followers), getCollectionCount(activityStreams.following), getCollectionCount(activityStreams.liked), @@ -145,21 +145,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Create user user = await User.insert({ - avatar_id: null, - banner_id: null, - created_at: new Date(), + avatarId: null, + bannerId: null, + createdAt: new Date(), description: summaryDOM.textContent, - followers_count, - following_count, + followersCount, + followingCount, name: activityStreams.name, - posts_count, + postsCount, likes_count, liked_count: 0, - drive_capacity: 1073741824, // 1GB + driveCapacity: 1073741824, // 1GB username: username, - username_lower, + usernameLower, host: toUnicode(finger.subject.replace(/^.*?@/, '')), - host_lower, + hostLower, account: { uri: activityStreams.id, }, @@ -182,18 +182,18 @@ module.exports = (params, me) => new Promise(async (res, rej) => { User.update({ _id: user._id }, { $set: { - avatar_id: icon._id, - banner_id: image._id, + avatarId: icon._id, + bannerId: image._id, }, }); - user.avatar_id = icon._id; - user.banner_id = icon._id; + user.avatarId = icon._id; + user.bannerId = icon._id; } } else { const q = userId !== undefined ? { _id: userId } - : { username_lower: username.toLowerCase(), host: null }; + : { usernameLower: username.toLowerCase(), host: null }; user = await findUser(q); diff --git a/src/server/api/models/access-token.ts b/src/server/api/models/access-token.ts index 2bf91f3093..59bb094269 100644 --- a/src/server/api/models/access-token.ts +++ b/src/server/api/models/access-token.ts @@ -1,8 +1,16 @@ +import * as mongo from 'mongodb'; import db from '../../../db/mongodb'; -const collection = db.get('access_tokens'); +const AccessToken = db.get('accessTokens'); +AccessToken.createIndex('token'); +AccessToken.createIndex('hash'); +export default AccessToken; -(collection as any).createIndex('token'); // fuck type definition -(collection as any).createIndex('hash'); // fuck type definition - -export default collection as any; // fuck type definition +export type IAccessTokens = { + _id: mongo.ObjectID; + createdAt: Date; + appId: mongo.ObjectID; + userId: mongo.ObjectID; + token: string; + hash: string; +}; diff --git a/src/server/api/models/app.ts b/src/server/api/models/app.ts index 17db82ecac..3c17c50fdf 100644 --- a/src/server/api/models/app.ts +++ b/src/server/api/models/app.ts @@ -5,16 +5,22 @@ import db from '../../../db/mongodb'; import config from '../../../conf'; const App = db.get('apps'); -App.createIndex('name_id'); -App.createIndex('name_id_lower'); +App.createIndex('nameId'); +App.createIndex('nameIdLower'); App.createIndex('secret'); export default App; export type IApp = { _id: mongo.ObjectID; - created_at: Date; - user_id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; secret: string; + name: string; + nameId: string; + nameIdLower: string; + description: string; + permission: string; + callbackUrl: string; }; export function isValidNameId(nameId: string): boolean { @@ -70,27 +76,27 @@ export const pack = ( _app.id = _app._id; delete _app._id; - delete _app.name_id_lower; + delete _app.nameIdLower; // Visible by only owner if (!opts.includeSecret) { delete _app.secret; } - _app.icon_url = _app.icon != null + _app.iconUrl = _app.icon != null ? `${config.drive_url}/${_app.icon}` : `${config.drive_url}/app-default.jpg`; if (me) { // 既に連携しているか const exist = await AccessToken.count({ - app_id: _app.id, - user_id: me, + appId: _app.id, + userId: me, }, { limit: 1 }); - _app.is_authorized = exist === 1; + _app.isAuthorized = exist === 1; } resolve(_app); diff --git a/src/server/api/models/appdata.ts b/src/server/api/models/appdata.ts deleted file mode 100644 index dda3c98934..0000000000 --- a/src/server/api/models/appdata.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../../db/mongodb'; - -export default db.get('appdata') as any; // fuck type definition diff --git a/src/server/api/models/auth-session.ts b/src/server/api/models/auth-session.ts index a79d901df5..2da40b1ea9 100644 --- a/src/server/api/models/auth-session.ts +++ b/src/server/api/models/auth-session.ts @@ -3,11 +3,15 @@ import deepcopy = require('deepcopy'); import db from '../../../db/mongodb'; import { pack as packApp } from './app'; -const AuthSession = db.get('auth_sessions'); +const AuthSession = db.get('authSessions'); export default AuthSession; export interface IAuthSession { _id: mongo.ObjectID; + createdAt: Date; + appId: mongo.ObjectID; + userId: mongo.ObjectID; + token: string; } /** @@ -24,7 +28,6 @@ export const pack = ( let _session: any; // TODO: Populate session if it ID - _session = deepcopy(session); // Me @@ -39,7 +42,7 @@ export const pack = ( delete _session._id; // Populate app - _session.app = await packApp(_session.app_id, me); + _session.app = await packApp(_session.appId, me); resolve(_session); }); diff --git a/src/server/api/models/channel-watching.ts b/src/server/api/models/channel-watching.ts index 4c6fae28d3..a26b7edb91 100644 --- a/src/server/api/models/channel-watching.ts +++ b/src/server/api/models/channel-watching.ts @@ -1,3 +1,13 @@ +import * as mongo from 'mongodb'; import db from '../../../db/mongodb'; -export default db.get('channel_watching') as any; // fuck type definition +const ChannelWatching = db.get('channelWatching'); +export default ChannelWatching; + +export interface IChannelWatching { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + channelId: mongo.ObjectID; + userId: mongo.ObjectID; +} diff --git a/src/server/api/models/channel.ts b/src/server/api/models/channel.ts index 97999bd9e2..9f94c5a8d1 100644 --- a/src/server/api/models/channel.ts +++ b/src/server/api/models/channel.ts @@ -9,10 +9,11 @@ export default Channel; export type IChannel = { _id: mongo.ObjectID; - created_at: Date; + createdAt: Date; title: string; - user_id: mongo.ObjectID; + userId: mongo.ObjectID; index: number; + watchingCount: number; }; /** @@ -47,7 +48,7 @@ export const pack = ( delete _channel._id; // Remove needless properties - delete _channel.user_id; + delete _channel.userId; // Me const meId: mongo.ObjectID = me @@ -61,12 +62,12 @@ export const pack = ( if (me) { //#region Watchしているかどうか const watch = await Watching.findOne({ - user_id: meId, - channel_id: _channel.id, - deleted_at: { $exists: false } + userId: meId, + channelId: _channel.id, + deletedAt: { $exists: false } }); - _channel.is_watching = watch !== null; + _channel.isWatching = watch !== null; //#endregion } diff --git a/src/server/api/models/drive-file.ts b/src/server/api/models/drive-file.ts index 851a79a0e7..04c9c54bd4 100644 --- a/src/server/api/models/drive-file.ts +++ b/src/server/api/models/drive-file.ts @@ -4,14 +4,14 @@ import { pack as packFolder } from './drive-folder'; import config from '../../../conf'; import monkDb, { nativeDbConn } from '../../../db/mongodb'; -const DriveFile = monkDb.get('drive_files.files'); +const DriveFile = monkDb.get('driveFiles.files'); export default DriveFile; const getGridFSBucket = async (): Promise => { const db = await nativeDbConn(); const bucket = new mongodb.GridFSBucket(db, { - bucketName: 'drive_files' + bucketName: 'driveFiles' }); return bucket; }; @@ -26,8 +26,8 @@ export type IDriveFile = { contentType: string; metadata: { properties: any; - user_id: mongodb.ObjectID; - folder_id: mongodb.ObjectID; + userId: mongodb.ObjectID; + folderId: mongodb.ObjectID; } }; @@ -79,7 +79,7 @@ export const pack = ( let _target: any = {}; _target.id = _file._id; - _target.created_at = _file.uploadDate; + _target.createdAt = _file.uploadDate; _target.name = _file.filename; _target.type = _file.contentType; _target.datasize = _file.length; @@ -92,9 +92,9 @@ export const pack = ( if (_target.properties == null) _target.properties = {}; if (opts.detail) { - if (_target.folder_id) { + if (_target.folderId) { // Populate folder - _target.folder = await packFolder(_target.folder_id, { + _target.folder = await packFolder(_target.folderId, { detail: true }); } diff --git a/src/server/api/models/drive-folder.ts b/src/server/api/models/drive-folder.ts index 505556376a..4ecafaa154 100644 --- a/src/server/api/models/drive-folder.ts +++ b/src/server/api/models/drive-folder.ts @@ -8,10 +8,10 @@ export default DriveFolder; export type IDriveFolder = { _id: mongo.ObjectID; - created_at: Date; + createdAt: Date; name: string; - user_id: mongo.ObjectID; - parent_id: mongo.ObjectID; + userId: mongo.ObjectID; + parentId: mongo.ObjectID; }; export function isValidFolderName(name: string): boolean { @@ -55,20 +55,20 @@ export const pack = ( if (opts.detail) { const childFoldersCount = await DriveFolder.count({ - parent_id: _folder.id + parentId: _folder.id }); const childFilesCount = await DriveFile.count({ - 'metadata.folder_id': _folder.id + 'metadata.folderId': _folder.id }); - _folder.folders_count = childFoldersCount; - _folder.files_count = childFilesCount; + _folder.foldersCount = childFoldersCount; + _folder.filesCount = childFilesCount; } - if (opts.detail && _folder.parent_id) { + if (opts.detail && _folder.parentId) { // Populate parent folder - _folder.parent = await pack(_folder.parent_id, { + _folder.parent = await pack(_folder.parentId, { detail: true }); } diff --git a/src/server/api/models/drive-tag.ts b/src/server/api/models/drive-tag.ts deleted file mode 100644 index d1c68365a3..0000000000 --- a/src/server/api/models/drive-tag.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../../db/mongodb'; - -export default db.get('drive_tags') as any; // fuck type definition diff --git a/src/server/api/models/favorite.ts b/src/server/api/models/favorite.ts index 3142617643..5fb4db95a9 100644 --- a/src/server/api/models/favorite.ts +++ b/src/server/api/models/favorite.ts @@ -1,3 +1,12 @@ +import * as mongo from 'mongodb'; import db from '../../../db/mongodb'; -export default db.get('favorites') as any; // fuck type definition +const Favorites = db.get('favorites'); +export default Favorites; + +export type IFavorite = { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + postId: mongo.ObjectID; +}; diff --git a/src/server/api/models/following.ts b/src/server/api/models/following.ts index 92d7b6d31b..552e946049 100644 --- a/src/server/api/models/following.ts +++ b/src/server/api/models/following.ts @@ -1,3 +1,13 @@ +import * as mongo from 'mongodb'; import db from '../../../db/mongodb'; -export default db.get('following') as any; // fuck type definition +const Following = db.get('following'); +export default Following; + +export type IFollowing = { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + followeeId: mongo.ObjectID; + followerId: mongo.ObjectID; +}; diff --git a/src/server/api/models/messaging-history.ts b/src/server/api/models/messaging-history.ts index ea9f317eee..44a2adc315 100644 --- a/src/server/api/models/messaging-history.ts +++ b/src/server/api/models/messaging-history.ts @@ -1,3 +1,13 @@ +import * as mongo from 'mongodb'; import db from '../../../db/mongodb'; -export default db.get('messaging_histories') as any; // fuck type definition +const MessagingHistory = db.get('messagingHistories'); +export default MessagingHistory; + +export type IMessagingHistory = { + _id: mongo.ObjectID; + updatedAt: Date; + userId: mongo.ObjectID; + partnerId: mongo.ObjectID; + messageId: mongo.ObjectID; +}; diff --git a/src/server/api/models/messaging-message.ts b/src/server/api/models/messaging-message.ts index be484d635f..d3a418c9a6 100644 --- a/src/server/api/models/messaging-message.ts +++ b/src/server/api/models/messaging-message.ts @@ -5,16 +5,17 @@ import { pack as packFile } from './drive-file'; import db from '../../../db/mongodb'; import parse from '../common/text'; -const MessagingMessage = db.get('messaging_messages'); +const MessagingMessage = db.get('messagingMessages'); export default MessagingMessage; export interface IMessagingMessage { _id: mongo.ObjectID; - created_at: Date; + createdAt: Date; text: string; - user_id: mongo.ObjectID; - recipient_id: mongo.ObjectID; - is_read: boolean; + userId: mongo.ObjectID; + recipientId: mongo.ObjectID; + isRead: boolean; + fileId: mongo.ObjectID; } export function isValidText(text: string): boolean { @@ -65,16 +66,16 @@ export const pack = ( } // Populate user - _message.user = await packUser(_message.user_id, me); + _message.user = await packUser(_message.userId, me); - if (_message.file_id) { + if (_message.fileId) { // Populate file - _message.file = await packFile(_message.file_id); + _message.file = await packFile(_message.fileId); } if (opts.populateRecipient) { // Populate recipient - _message.recipient = await packUser(_message.recipient_id, me); + _message.recipient = await packUser(_message.recipientId, me); } resolve(_message); diff --git a/src/server/api/models/meta.ts b/src/server/api/models/meta.ts index ee1ada18fa..cad7f5096e 100644 --- a/src/server/api/models/meta.ts +++ b/src/server/api/models/meta.ts @@ -1,7 +1,8 @@ import db from '../../../db/mongodb'; -export default db.get('meta') as any; // fuck type definition +const Meta = db.get('meta'); +export default Meta; export type IMeta = { - top_image: string; + broadcasts: any[]; }; diff --git a/src/server/api/models/mute.ts b/src/server/api/models/mute.ts index 02f652c30b..e5385ade39 100644 --- a/src/server/api/models/mute.ts +++ b/src/server/api/models/mute.ts @@ -1,3 +1,13 @@ +import * as mongo from 'mongodb'; import db from '../../../db/mongodb'; -export default db.get('mute') as any; // fuck type definition +const Mute = db.get('mute'); +export default Mute; + +export interface IMute { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + muterId: mongo.ObjectID; + muteeId: mongo.ObjectID; +} diff --git a/src/server/api/models/notification.ts b/src/server/api/models/notification.ts index bcb25534dc..237e2663fc 100644 --- a/src/server/api/models/notification.ts +++ b/src/server/api/models/notification.ts @@ -9,7 +9,7 @@ export default Notification; export interface INotification { _id: mongo.ObjectID; - created_at: Date; + createdAt: Date; /** * 通知の受信者 @@ -19,7 +19,7 @@ export interface INotification { /** * 通知の受信者 */ - notifiee_id: mongo.ObjectID; + notifieeId: mongo.ObjectID; /** * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー @@ -29,7 +29,7 @@ export interface INotification { /** * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー */ - notifier_id: mongo.ObjectID; + notifierId: mongo.ObjectID; /** * 通知の種類。 @@ -46,7 +46,7 @@ export interface INotification { /** * 通知が読まれたかどうか */ - is_read: Boolean; + isRead: Boolean; } /** @@ -75,15 +75,15 @@ export const pack = (notification: any) => new Promise(async (resolve, reje _notification.id = _notification._id; delete _notification._id; - // Rename notifier_id to user_id - _notification.user_id = _notification.notifier_id; - delete _notification.notifier_id; + // Rename notifierId to userId + _notification.userId = _notification.notifierId; + delete _notification.notifierId; - const me = _notification.notifiee_id; - delete _notification.notifiee_id; + const me = _notification.notifieeId; + delete _notification.notifieeId; // Populate notifier - _notification.user = await packUser(_notification.user_id, me); + _notification.user = await packUser(_notification.userId, me); switch (_notification.type) { case 'follow': @@ -96,7 +96,7 @@ export const pack = (notification: any) => new Promise(async (resolve, reje case 'reaction': case 'poll_vote': // Populate post - _notification.post = await packPost(_notification.post_id, me); + _notification.post = await packPost(_notification.postId, me); break; default: console.error(`Unknown type: ${_notification.type}`); diff --git a/src/server/api/models/othello-game.ts b/src/server/api/models/othello-game.ts index 97508e46da..ebe7388150 100644 --- a/src/server/api/models/othello-game.ts +++ b/src/server/api/models/othello-game.ts @@ -3,17 +3,17 @@ import deepcopy = require('deepcopy'); import db from '../../../db/mongodb'; import { IUser, pack as packUser } from './user'; -const Game = db.get('othello_games'); -export default Game; +const OthelloGame = db.get('othelloGames'); +export default OthelloGame; -export interface IGame { +export interface IOthelloGame { _id: mongo.ObjectID; - created_at: Date; - started_at: Date; - user1_id: mongo.ObjectID; - user2_id: mongo.ObjectID; - user1_accepted: boolean; - user2_accepted: boolean; + createdAt: Date; + startedAt: Date; + user1Id: mongo.ObjectID; + user2Id: mongo.ObjectID; + user1Accepted: boolean; + user2Accepted: boolean; /** * どちらのプレイヤーが先行(黒)か @@ -22,9 +22,9 @@ export interface IGame { */ black: number; - is_started: boolean; - is_ended: boolean; - winner_id: mongo.ObjectID; + isStarted: boolean; + isEnded: boolean; + winnerId: mongo.ObjectID; logs: Array<{ at: Date; color: boolean; @@ -33,9 +33,9 @@ export interface IGame { settings: { map: string[]; bw: string | number; - is_llotheo: boolean; - can_put_everywhere: boolean; - looped_board: boolean; + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; }; form1: any; form2: any; @@ -62,11 +62,11 @@ export const pack = ( // Populate the game if 'game' is ID if (mongo.ObjectID.prototype.isPrototypeOf(game)) { - _game = await Game.findOne({ + _game = await OthelloGame.findOne({ _id: game }); } else if (typeof game === 'string') { - _game = await Game.findOne({ + _game = await OthelloGame.findOne({ _id: new mongo.ObjectID(game) }); } else { @@ -97,10 +97,10 @@ export const pack = ( } // Populate user - _game.user1 = await packUser(_game.user1_id, meId); - _game.user2 = await packUser(_game.user2_id, meId); - if (_game.winner_id) { - _game.winner = await packUser(_game.winner_id, meId); + _game.user1 = await packUser(_game.user1Id, meId); + _game.user2 = await packUser(_game.user2Id, meId); + if (_game.winnerId) { + _game.winner = await packUser(_game.winnerId, meId); } else { _game.winner = null; } diff --git a/src/server/api/models/othello-matching.ts b/src/server/api/models/othello-matching.ts index 3c29e6a00c..a294bd1ef9 100644 --- a/src/server/api/models/othello-matching.ts +++ b/src/server/api/models/othello-matching.ts @@ -3,14 +3,14 @@ import deepcopy = require('deepcopy'); import db from '../../../db/mongodb'; import { IUser, pack as packUser } from './user'; -const Matching = db.get('othello_matchings'); +const Matching = db.get('othelloMatchings'); export default Matching; export interface IMatching { _id: mongo.ObjectID; - created_at: Date; - parent_id: mongo.ObjectID; - child_id: mongo.ObjectID; + createdAt: Date; + parentId: mongo.ObjectID; + childId: mongo.ObjectID; } /** @@ -37,8 +37,8 @@ export const pack = ( delete _matching._id; // Populate user - _matching.parent = await packUser(_matching.parent_id, meId); - _matching.child = await packUser(_matching.child_id, meId); + _matching.parent = await packUser(_matching.parentId, meId); + _matching.child = await packUser(_matching.childId, meId); resolve(_matching); }); diff --git a/src/server/api/models/poll-vote.ts b/src/server/api/models/poll-vote.ts index c6638ccf1c..1cad95e5de 100644 --- a/src/server/api/models/poll-vote.ts +++ b/src/server/api/models/poll-vote.ts @@ -1,3 +1,17 @@ +<<<<<<< HEAD:src/server/api/models/poll-vote.ts import db from '../../../db/mongodb'; +======= +import * as mongo from 'mongodb'; +import db from '../../db/mongodb'; +>>>>>>> refs/remotes/origin/master:src/api/models/poll-vote.ts -export default db.get('poll_votes') as any; // fuck type definition +const PollVote = db.get('pollVotes'); +export default PollVote; + +export interface IPollVote { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + postId: mongo.ObjectID; + choice: number; +} diff --git a/src/server/api/models/post-reaction.ts b/src/server/api/models/post-reaction.ts index 5cd122d76b..f9a3f91c21 100644 --- a/src/server/api/models/post-reaction.ts +++ b/src/server/api/models/post-reaction.ts @@ -4,13 +4,15 @@ import db from '../../../db/mongodb'; import Reaction from './post-reaction'; import { pack as packUser } from './user'; -const PostReaction = db.get('post_reactions'); +const PostReaction = db.get('postReactions'); export default PostReaction; export interface IPostReaction { _id: mongo.ObjectID; - created_at: Date; - deleted_at: Date; + createdAt: Date; + deletedAt: Date; + postId: mongo.ObjectID; + userId: mongo.ObjectID; reaction: string; } @@ -45,7 +47,7 @@ export const pack = ( delete _reaction._id; // Populate user - _reaction.user = await packUser(_reaction.user_id, me); + _reaction.user = await packUser(_reaction.userId, me); resolve(_reaction); }); diff --git a/src/server/api/models/post-watching.ts b/src/server/api/models/post-watching.ts index 9a4163c8dc..abd6322495 100644 --- a/src/server/api/models/post-watching.ts +++ b/src/server/api/models/post-watching.ts @@ -1,3 +1,12 @@ +import * as mongo from 'mongodb'; import db from '../../../db/mongodb'; -export default db.get('post_watching') as any; // fuck type definition +const PostWatching = db.get('postWatching'); +export default PostWatching; + +export interface IPostWatching { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + postId: mongo.ObjectID; +} diff --git a/src/server/api/models/post.ts b/src/server/api/models/post.ts index 3f648e08cd..0317cff3fa 100644 --- a/src/server/api/models/post.ts +++ b/src/server/api/models/post.ts @@ -20,18 +20,20 @@ export function isValidText(text: string): boolean { export type IPost = { _id: mongo.ObjectID; - channel_id: mongo.ObjectID; - created_at: Date; - media_ids: mongo.ObjectID[]; - reply_id: mongo.ObjectID; - repost_id: mongo.ObjectID; + channelId: mongo.ObjectID; + createdAt: Date; + mediaIds: mongo.ObjectID[]; + replyId: mongo.ObjectID; + repostId: mongo.ObjectID; poll: any; // todo text: string; - user_id: mongo.ObjectID; - app_id: mongo.ObjectID; - category: string; - is_category_verified: boolean; - via_mobile: boolean; + userId: mongo.ObjectID; + appId: mongo.ObjectID; + viaMobile: boolean; + repostCount: number; + repliesCount: number; + reactionCounts: any; + mentions: mongo.ObjectID[]; geo: { latitude: number; longitude: number; @@ -102,21 +104,21 @@ export const pack = async ( } // Populate user - _post.user = packUser(_post.user_id, meId); + _post.user = packUser(_post.userId, meId); // Populate app - if (_post.app_id) { - _post.app = packApp(_post.app_id); + if (_post.appId) { + _post.app = packApp(_post.appId); } // Populate channel - if (_post.channel_id) { - _post.channel = packChannel(_post.channel_id); + if (_post.channelId) { + _post.channel = packChannel(_post.channelId); } // Populate media - if (_post.media_ids) { - _post.media = Promise.all(_post.media_ids.map(fileId => + if (_post.mediaIds) { + _post.media = Promise.all(_post.mediaIds.map(fileId => packFile(fileId) )); } @@ -126,7 +128,7 @@ export const pack = async ( // Get previous post info _post.prev = (async () => { const prev = await Post.findOne({ - user_id: _post.user_id, + userId: _post.userId, _id: { $lt: id } @@ -144,7 +146,7 @@ export const pack = async ( // Get next post info _post.next = (async () => { const next = await Post.findOne({ - user_id: _post.user_id, + userId: _post.userId, _id: { $gt: id } @@ -159,16 +161,16 @@ export const pack = async ( return next ? next._id : null; })(); - if (_post.reply_id) { + if (_post.replyId) { // Populate reply to post - _post.reply = pack(_post.reply_id, meId, { + _post.reply = pack(_post.replyId, meId, { detail: false }); } - if (_post.repost_id) { + if (_post.repostId) { // Populate repost - _post.repost = pack(_post.repost_id, meId, { + _post.repost = pack(_post.repostId, meId, { detail: _post.text == null }); } @@ -178,15 +180,15 @@ export const pack = async ( _post.poll = (async (poll) => { const vote = await Vote .findOne({ - user_id: meId, - post_id: id + userId: meId, + postId: id }); if (vote != null) { const myChoice = poll.choices .filter(c => c.id == vote.choice)[0]; - myChoice.is_voted = true; + myChoice.isVoted = true; } return poll; @@ -195,12 +197,12 @@ export const pack = async ( // Fetch my reaction if (meId) { - _post.my_reaction = (async () => { + _post.myReaction = (async () => { const reaction = await Reaction .findOne({ - user_id: meId, - post_id: id, - deleted_at: { $exists: false } + userId: meId, + postId: id, + deletedAt: { $exists: false } }); if (reaction) { diff --git a/src/server/api/models/signin.ts b/src/server/api/models/signin.ts index 5cffb3c310..bec6359477 100644 --- a/src/server/api/models/signin.ts +++ b/src/server/api/models/signin.ts @@ -7,6 +7,11 @@ export default Signin; export interface ISignin { _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + ip: string; + headers: any; + success: boolean; } /** diff --git a/src/server/api/models/sw-subscription.ts b/src/server/api/models/sw-subscription.ts index 4506a982f2..d3bbd75a6d 100644 --- a/src/server/api/models/sw-subscription.ts +++ b/src/server/api/models/sw-subscription.ts @@ -1,3 +1,13 @@ +import * as mongo from 'mongodb'; import db from '../../../db/mongodb'; -export default db.get('sw_subscriptions') as any; // fuck type definition +const SwSubscription = db.get('swSubscriptions'); +export default SwSubscription; + +export interface ISwSubscription { + _id: mongo.ObjectID; + userId: mongo.ObjectID; + endpoint: string; + auth: string; + publickey: string; +} diff --git a/src/server/api/models/user.ts b/src/server/api/models/user.ts index 8e7d50baa3..419ad53970 100644 --- a/src/server/api/models/user.ts +++ b/src/server/api/models/user.ts @@ -46,25 +46,26 @@ export type ILocalAccount = { password: string; token: string; twitter: { - access_token: string; - access_token_secret: string; - user_id: string; - screen_name: string; + accessToken: string; + accessTokenSecret: string; + userId: string; + screenName: string; }; line: { - user_id: string; + userId: string; }; profile: { location: string; birthday: string; // 'YYYY-MM-DD' tags: string[]; }; - last_used_at: Date; - is_bot: boolean; - is_pro: boolean; - two_factor_secret: string; - two_factor_enabled: boolean; - client_settings: any; + lastUsedAt: Date; + isBot: boolean; + isPro: boolean; + twoFactorSecret: string; + twoFactorEnabled: boolean; + twoFactorTempSecret: string; + clientSettings: any; settings: any; }; @@ -74,33 +75,33 @@ export type IRemoteAccount = { export type IUser = { _id: mongo.ObjectID; - created_at: Date; - deleted_at: Date; - followers_count: number; - following_count: number; + createdAt: Date; + deletedAt: Date; + followersCount: number; + followingCount: number; name: string; - posts_count: number; - drive_capacity: number; + postsCount: number; + driveCapacity: number; username: string; - username_lower: string; - avatar_id: mongo.ObjectID; - banner_id: mongo.ObjectID; + usernameLower: string; + avatarId: mongo.ObjectID; + bannerId: mongo.ObjectID; data: any; description: string; - latest_post: IPost; - pinned_post_id: mongo.ObjectID; - is_suspended: boolean; + latestPost: IPost; + pinnedPostId: mongo.ObjectID; + isSuspended: boolean; keywords: string[]; host: string; - host_lower: string; + hostLower: string; account: ILocalAccount | IRemoteAccount; }; export function init(user): IUser { user._id = new mongo.ObjectID(user._id); - user.avatar_id = new mongo.ObjectID(user.avatar_id); - user.banner_id = new mongo.ObjectID(user.banner_id); - user.pinned_post_id = new mongo.ObjectID(user.pinned_post_id); + user.avatarId = new mongo.ObjectID(user.avatarId); + user.bannerId = new mongo.ObjectID(user.bannerId); + user.pinnedPostId = new mongo.ObjectID(user.pinnedPostId); return user; } @@ -131,7 +132,7 @@ export const pack = ( const fields = opts.detail ? { } : { 'account.settings': false, - 'account.client_settings': false, + 'account.clientSettings': false, 'account.profile': false, 'account.keywords': false, 'account.domains': false @@ -166,19 +167,19 @@ export const pack = ( delete _user._id; // Remove needless properties - delete _user.latest_post; + delete _user.latestPost; if (!_user.host) { // Remove private properties delete _user.account.keypair; delete _user.account.password; delete _user.account.token; - delete _user.account.two_factor_temp_secret; - delete _user.account.two_factor_secret; - delete _user.username_lower; + delete _user.account.twoFactorTempSecret; + delete _user.account.twoFactorSecret; + delete _user.usernameLower; if (_user.account.twitter) { - delete _user.account.twitter.access_token; - delete _user.account.twitter.access_token_secret; + delete _user.account.twitter.accessToken; + delete _user.account.twitter.accessTokenSecret; } delete _user.account.line; @@ -186,65 +187,65 @@ export const pack = ( if (!opts.includeSecrets) { delete _user.account.email; delete _user.account.settings; - delete _user.account.client_settings; + delete _user.account.clientSettings; } if (!opts.detail) { - delete _user.account.two_factor_enabled; + delete _user.account.twoFactorEnabled; } } - _user.avatar_url = _user.avatar_id != null - ? `${config.drive_url}/${_user.avatar_id}` + _user.avatarUrl = _user.avatarId != null + ? `${config.drive_url}/${_user.avatarId}` : `${config.drive_url}/default-avatar.jpg`; - _user.banner_url = _user.banner_id != null - ? `${config.drive_url}/${_user.banner_id}` + _user.bannerUrl = _user.bannerId != null + ? `${config.drive_url}/${_user.bannerId}` : null; if (!meId || !meId.equals(_user.id) || !opts.detail) { - delete _user.avatar_id; - delete _user.banner_id; + delete _user.avatarId; + delete _user.bannerId; - delete _user.drive_capacity; + delete _user.driveCapacity; } if (meId && !meId.equals(_user.id)) { // Whether the user is following - _user.is_following = (async () => { + _user.isFollowing = (async () => { const follow = await Following.findOne({ - follower_id: meId, - followee_id: _user.id, - deleted_at: { $exists: false } + followerId: meId, + followeeId: _user.id, + deletedAt: { $exists: false } }); return follow !== null; })(); // Whether the user is followed - _user.is_followed = (async () => { + _user.isFollowed = (async () => { const follow2 = await Following.findOne({ - follower_id: _user.id, - followee_id: meId, - deleted_at: { $exists: false } + followerId: _user.id, + followeeId: meId, + deletedAt: { $exists: false } }); return follow2 !== null; })(); // Whether the user is muted - _user.is_muted = (async () => { + _user.isMuted = (async () => { const mute = await Mute.findOne({ - muter_id: meId, - mutee_id: _user.id, - deleted_at: { $exists: false } + muterId: meId, + muteeId: _user.id, + deletedAt: { $exists: false } }); return mute !== null; })(); } if (opts.detail) { - if (_user.pinned_post_id) { + if (_user.pinnedPostId) { // Populate pinned post - _user.pinned_post = packPost(_user.pinned_post_id, meId, { + _user.pinnedPost = packPost(_user.pinnedPostId, meId, { detail: true }); } @@ -253,17 +254,17 @@ export const pack = ( const myFollowingIds = await getFriends(meId); // Get following you know count - _user.following_you_know_count = Following.count({ - followee_id: { $in: myFollowingIds }, - follower_id: _user.id, - deleted_at: { $exists: false } + _user.followingYouKnowCount = Following.count({ + followeeId: { $in: myFollowingIds }, + followerId: _user.id, + deletedAt: { $exists: false } }); // Get followers you know count - _user.followers_you_know_count = Following.count({ - followee_id: _user.id, - follower_id: { $in: myFollowingIds }, - deleted_at: { $exists: false } + _user.followersYouKnowCount = Following.count({ + followeeId: _user.id, + followerId: { $in: myFollowingIds }, + deletedAt: { $exists: false } }); } } @@ -322,7 +323,7 @@ export const packForAp = ( "name": _user.name, "summary": _user.description, "icon": [ - `${config.drive_url}/${_user.avatar_id}` + `${config.drive_url}/${_user.avatarId}` ] }); }); diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index bbc9908991..4b60f4c752 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -32,7 +32,7 @@ export default async (req: express.Request, res: express.Response) => { // Fetch user const user: IUser = await User.findOne({ - username_lower: username.toLowerCase(), + usernameLower: username.toLowerCase(), host: null }, { fields: { @@ -54,9 +54,9 @@ export default async (req: express.Request, res: express.Response) => { const same = await bcrypt.compare(password, account.password); if (same) { - if (account.two_factor_enabled) { + if (account.twoFactorEnabled) { const verified = (speakeasy as any).totp.verify({ - secret: account.two_factor_secret, + secret: account.twoFactorSecret, encoding: 'base32', token: token }); @@ -79,8 +79,8 @@ export default async (req: express.Request, res: express.Response) => { // Append signin history const record = await Signin.insert({ - created_at: new Date(), - user_id: user._id, + createdAt: new Date(), + userId: user._id, ip: req.ip, headers: req.headers, success: same diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index 9f55393313..cad9752c45 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -64,7 +64,7 @@ export default async (req: express.Request, res: express.Response) => { // Fetch exist user that same username const usernameExist = await User .count({ - username_lower: username.toLowerCase(), + usernameLower: username.toLowerCase(), host: null }, { limit: 1 @@ -107,21 +107,19 @@ export default async (req: express.Request, res: express.Response) => { // Create account const account: IUser = await User.insert({ - avatar_id: null, - banner_id: null, - created_at: new Date(), + avatarId: null, + bannerId: null, + createdAt: new Date(), description: null, - followers_count: 0, - following_count: 0, + followersCount: 0, + followingCount: 0, name: name, - posts_count: 0, - likes_count: 0, - liked_count: 0, - drive_capacity: 1073741824, // 1GB + postsCount: 0, + driveCapacity: 1073741824, // 1GB username: username, - username_lower: username.toLowerCase(), + usernameLower: username.toLowerCase(), host: null, - host_lower: null, + hostLower: null, account: { keypair: generateKeypair(), token: secret, @@ -139,9 +137,9 @@ export default async (req: express.Request, res: express.Response) => { weight: null }, settings: { - auto_watch: true + autoWatch: true }, - client_settings: { + clientSettings: { home: homeData } } diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index a33d359753..98732e6b85 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -9,7 +9,7 @@ module.exports = async (app: express.Application) => { if (config.github_bot == null) return; const bot = await User.findOne({ - username_lower: config.github_bot.username.toLowerCase() + usernameLower: config.github_bot.username.toLowerCase() }); if (bot == null) { diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts index 861f63ed67..bdbedc8646 100644 --- a/src/server/api/service/twitter.ts +++ b/src/server/api/service/twitter.ts @@ -128,7 +128,7 @@ module.exports = (app: express.Application) => { const user = await User.findOne({ host: null, - 'account.twitter.user_id': result.userId + 'account.twitter.userId': result.userId }); if (user == null) { @@ -155,10 +155,10 @@ module.exports = (app: express.Application) => { }, { $set: { 'account.twitter': { - access_token: result.accessToken, - access_token_secret: result.accessTokenSecret, - user_id: result.userId, - screen_name: result.screenName + accessToken: result.accessToken, + accessTokenSecret: result.accessTokenSecret, + userId: result.userId, + screenName: result.screenName } } }); diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts index 1ef0f33b4b..291be0824d 100644 --- a/src/server/api/stream/home.ts +++ b/src/server/api/stream/home.ts @@ -14,10 +14,10 @@ export default async function(request: websocket.request, connection: websocket. subscriber.subscribe(`misskey:user-stream:${user._id}`); const mute = await Mute.find({ - muter_id: user._id, - deleted_at: { $exists: false } + muterId: user._id, + deletedAt: { $exists: false } }); - const mutedUserIds = mute.map(m => m.mutee_id.toString()); + const mutedUserIds = mute.map(m => m.muteeId.toString()); subscriber.on('message', async (channel, data) => { switch (channel.split(':')[1]) { @@ -26,17 +26,17 @@ export default async function(request: websocket.request, connection: websocket. const x = JSON.parse(data); if (x.type == 'post') { - if (mutedUserIds.indexOf(x.body.user_id) != -1) { + if (mutedUserIds.indexOf(x.body.userId) != -1) { return; } - if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.user_id) != -1) { + if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.userId) != -1) { return; } - if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.user_id) != -1) { + if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.userId) != -1) { return; } } else if (x.type == 'notification') { - if (mutedUserIds.indexOf(x.body.user_id) != -1) { + if (mutedUserIds.indexOf(x.body.userId) != -1) { return; } } @@ -74,7 +74,7 @@ export default async function(request: websocket.request, connection: websocket. // Update lastUsedAt User.update({ _id: user._id }, { $set: { - 'account.last_used_at': new Date() + 'account.lastUsedAt': new Date() } }); break; diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts index 1c846f27ae..e48d93cddc 100644 --- a/src/server/api/stream/othello-game.ts +++ b/src/server/api/stream/othello-game.ts @@ -1,7 +1,7 @@ import * as websocket from 'websocket'; import * as redis from 'redis'; import * as CRC32 from 'crc-32'; -import Game, { pack } from '../models/othello-game'; +import OthelloGame, { pack } from '../models/othello-game'; import { publishOthelloGameStream } from '../event'; import Othello from '../../common/othello/core'; import * as maps from '../../common/othello/maps'; @@ -60,14 +60,14 @@ export default function(request: websocket.request, connection: websocket.connec }); async function updateSettings(settings) { - const game = await Game.findOne({ _id: gameId }); + const game = await OthelloGame.findOne({ _id: gameId }); - if (game.is_started) return; - if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return; - if (game.user1_id.equals(user._id) && game.user1_accepted) return; - if (game.user2_id.equals(user._id) && game.user2_accepted) return; + if (game.isStarted) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; + if (game.user1Id.equals(user._id) && game.user1Accepted) return; + if (game.user2Id.equals(user._id) && game.user2Accepted) return; - await Game.update({ _id: gameId }, { + await OthelloGame.update({ _id: gameId }, { $set: { settings } @@ -77,34 +77,34 @@ export default function(request: websocket.request, connection: websocket.connec } async function initForm(form) { - const game = await Game.findOne({ _id: gameId }); + const game = await OthelloGame.findOne({ _id: gameId }); - if (game.is_started) return; - if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return; + if (game.isStarted) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; - const set = game.user1_id.equals(user._id) ? { + const set = game.user1Id.equals(user._id) ? { form1: form } : { form2: form }; - await Game.update({ _id: gameId }, { + await OthelloGame.update({ _id: gameId }, { $set: set }); publishOthelloGameStream(gameId, 'init-form', { - user_id: user._id, + userId: user._id, form }); } async function updateForm(id, value) { - const game = await Game.findOne({ _id: gameId }); + const game = await OthelloGame.findOne({ _id: gameId }); - if (game.is_started) return; - if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return; + if (game.isStarted) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; - const form = game.user1_id.equals(user._id) ? game.form2 : game.form1; + const form = game.user1Id.equals(user._id) ? game.form2 : game.form1; const item = form.find(i => i.id == id); @@ -112,18 +112,18 @@ export default function(request: websocket.request, connection: websocket.connec item.value = value; - const set = game.user1_id.equals(user._id) ? { + const set = game.user1Id.equals(user._id) ? { form2: form } : { form1: form }; - await Game.update({ _id: gameId }, { + await OthelloGame.update({ _id: gameId }, { $set: set }); publishOthelloGameStream(gameId, 'update-form', { - user_id: user._id, + userId: user._id, id, value }); @@ -132,44 +132,44 @@ export default function(request: websocket.request, connection: websocket.connec async function message(message) { message.id = Math.random(); publishOthelloGameStream(gameId, 'message', { - user_id: user._id, + userId: user._id, message }); } async function accept(accept: boolean) { - const game = await Game.findOne({ _id: gameId }); + const game = await OthelloGame.findOne({ _id: gameId }); - if (game.is_started) return; + if (game.isStarted) return; let bothAccepted = false; - if (game.user1_id.equals(user._id)) { - await Game.update({ _id: gameId }, { + if (game.user1Id.equals(user._id)) { + await OthelloGame.update({ _id: gameId }, { $set: { - user1_accepted: accept + user1Accepted: accept } }); publishOthelloGameStream(gameId, 'change-accepts', { user1: accept, - user2: game.user2_accepted + user2: game.user2Accepted }); - if (accept && game.user2_accepted) bothAccepted = true; - } else if (game.user2_id.equals(user._id)) { - await Game.update({ _id: gameId }, { + if (accept && game.user2Accepted) bothAccepted = true; + } else if (game.user2Id.equals(user._id)) { + await OthelloGame.update({ _id: gameId }, { $set: { - user2_accepted: accept + user2Accepted: accept } }); publishOthelloGameStream(gameId, 'change-accepts', { - user1: game.user1_accepted, + user1: game.user1Accepted, user2: accept }); - if (accept && game.user1_accepted) bothAccepted = true; + if (accept && game.user1Accepted) bothAccepted = true; } else { return; } @@ -177,9 +177,9 @@ export default function(request: websocket.request, connection: websocket.connec if (bothAccepted) { // 3秒後、まだacceptされていたらゲーム開始 setTimeout(async () => { - const freshGame = await Game.findOne({ _id: gameId }); - if (freshGame == null || freshGame.is_started || freshGame.is_ended) return; - if (!freshGame.user1_accepted || !freshGame.user2_accepted) return; + const freshGame = await OthelloGame.findOne({ _id: gameId }); + if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; + if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; let bw: number; if (freshGame.settings.bw == 'random') { @@ -196,10 +196,10 @@ export default function(request: websocket.request, connection: websocket.connec const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); - await Game.update({ _id: gameId }, { + await OthelloGame.update({ _id: gameId }, { $set: { - started_at: new Date(), - is_started: true, + startedAt: new Date(), + isStarted: true, black: bw, 'settings.map': map } @@ -207,32 +207,32 @@ export default function(request: websocket.request, connection: websocket.connec //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 const o = new Othello(map, { - isLlotheo: freshGame.settings.is_llotheo, - canPutEverywhere: freshGame.settings.can_put_everywhere, - loopedBoard: freshGame.settings.looped_board + isLlotheo: freshGame.settings.isLlotheo, + canPutEverywhere: freshGame.settings.canPutEverywhere, + loopedBoard: freshGame.settings.loopedBoard }); if (o.isEnded) { let winner; if (o.winner === true) { - winner = freshGame.black == 1 ? freshGame.user1_id : freshGame.user2_id; + winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; } else if (o.winner === false) { - winner = freshGame.black == 1 ? freshGame.user2_id : freshGame.user1_id; + winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; } else { winner = null; } - await Game.update({ + await OthelloGame.update({ _id: gameId }, { $set: { - is_ended: true, - winner_id: winner + isEnded: true, + winnerId: winner } }); publishOthelloGameStream(gameId, 'ended', { - winner_id: winner, + winnerId: winner, game: await pack(gameId, user) }); } @@ -245,16 +245,16 @@ export default function(request: websocket.request, connection: websocket.connec // 石を打つ async function set(pos) { - const game = await Game.findOne({ _id: gameId }); + const game = await OthelloGame.findOne({ _id: gameId }); - if (!game.is_started) return; - if (game.is_ended) return; - if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return; + if (!game.isStarted) return; + if (game.isEnded) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; const o = new Othello(game.settings.map, { - isLlotheo: game.settings.is_llotheo, - canPutEverywhere: game.settings.can_put_everywhere, - loopedBoard: game.settings.looped_board + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard }); game.logs.forEach(log => { @@ -262,7 +262,7 @@ export default function(request: websocket.request, connection: websocket.connec }); const myColor = - (game.user1_id.equals(user._id) && game.black == 1) || (game.user2_id.equals(user._id) && game.black == 2) + (game.user1Id.equals(user._id) && game.black == 1) || (game.user2Id.equals(user._id) && game.black == 2) ? true : false; @@ -272,9 +272,9 @@ export default function(request: websocket.request, connection: websocket.connec let winner; if (o.isEnded) { if (o.winner === true) { - winner = game.black == 1 ? game.user1_id : game.user2_id; + winner = game.black == 1 ? game.user1Id : game.user2Id; } else if (o.winner === false) { - winner = game.black == 1 ? game.user2_id : game.user1_id; + winner = game.black == 1 ? game.user2Id : game.user1Id; } else { winner = null; } @@ -288,13 +288,13 @@ export default function(request: websocket.request, connection: websocket.connec const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); - await Game.update({ + await OthelloGame.update({ _id: gameId }, { $set: { crc32, - is_ended: o.isEnded, - winner_id: winner + isEnded: o.isEnded, + winnerId: winner }, $push: { logs: log @@ -307,16 +307,16 @@ export default function(request: websocket.request, connection: websocket.connec if (o.isEnded) { publishOthelloGameStream(gameId, 'ended', { - winner_id: winner, + winnerId: winner, game: await pack(gameId, user) }); } } async function check(crc32) { - const game = await Game.findOne({ _id: gameId }); + const game = await OthelloGame.findOne({ _id: gameId }); - if (!game.is_started) return; + if (!game.isStarted) return; // 互換性のため if (game.crc32 == null) return; diff --git a/src/server/api/stream/othello.ts b/src/server/api/stream/othello.ts index bd3b4a7637..55c993ec85 100644 --- a/src/server/api/stream/othello.ts +++ b/src/server/api/stream/othello.ts @@ -18,11 +18,11 @@ export default function(request: websocket.request, connection: websocket.connec case 'ping': if (msg.id == null) return; const matching = await Matching.findOne({ - parent_id: user._id, - child_id: new mongo.ObjectID(msg.id) + parentId: user._id, + childId: new mongo.ObjectID(msg.id) }); if (matching == null) return; - publishUserStream(matching.child_id, 'othello_invited', await pack(matching, matching.child_id)); + publishUserStream(matching.childId, 'othello_invited', await pack(matching, matching.childId)); break; } }); diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index 95f444e00b..73f099bd80 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -110,7 +110,7 @@ function authenticate(token: string): Promise { // Fetch user const user: IUser = await User - .findOne({ _id: accessToken.user_id }); + .findOne({ _id: accessToken.userId }); resolve(user); } diff --git a/src/server/common/get-post-summary.ts b/src/server/common/get-post-summary.ts index 6e8f65708e..8d0033064f 100644 --- a/src/server/common/get-post-summary.ts +++ b/src/server/common/get-post-summary.ts @@ -22,7 +22,7 @@ const summarize = (post: any): string => { } // 返信のとき - if (post.reply_id) { + if (post.replyId) { if (post.reply) { summary += ` RE: ${summarize(post.reply)}`; } else { @@ -31,7 +31,7 @@ const summarize = (post: any): string => { } // Repostのとき - if (post.repost_id) { + if (post.repostId) { if (post.repost) { summary += ` RP: ${summarize(post.repost)}`; } else { diff --git a/src/server/common/othello/ai/back.ts b/src/server/common/othello/ai/back.ts index c20c6fed25..629e57113b 100644 --- a/src/server/common/othello/ai/back.ts +++ b/src/server/common/othello/ai/back.ts @@ -44,7 +44,7 @@ process.on('message', async msg => { //#region TLに投稿する const game = msg.body; const url = `${conf.url}/othello/${game.id}`; - const user = game.user1_id == id ? game.user2 : game.user1; + const user = game.user1Id == id ? game.user2 : game.user1; const isSettai = form[0].value === 0; const text = isSettai ? `?[${user.name}](${conf.url}/@${user.username})さんの接待を始めました!` @@ -56,7 +56,7 @@ process.on('message', async msg => { } }); - post = res.created_post; + post = res.createdPost; //#endregion } @@ -68,23 +68,23 @@ process.on('message', async msg => { }); //#region TLに投稿する - const user = game.user1_id == id ? game.user2 : game.user1; + const user = game.user1Id == id ? game.user2 : game.user1; const isSettai = form[0].value === 0; const text = isSettai - ? msg.body.winner_id === null + ? msg.body.winnerId === null ? `?[${user.name}](${conf.url}/@${user.username})さんに接待で引き分けました...` - : msg.body.winner_id == id + : msg.body.winnerId == id ? `?[${user.name}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...` : `?[${user.name}](${conf.url}/@${user.username})さんに接待で負けてあげました♪` - : msg.body.winner_id === null + : msg.body.winnerId === null ? `?[${user.name}](${conf.url}/@${user.username})さんと引き分けました~` - : msg.body.winner_id == id + : msg.body.winnerId == id ? `?[${user.name}](${conf.url}/@${user.username})さんに勝ちました♪` : `?[${user.name}](${conf.url}/@${user.username})さんに負けました...`; await request.post(`${conf.api_url}/posts/create`, { json: { i, - repost_id: post.id, + repostId: post.id, text: text } }); @@ -114,9 +114,9 @@ function onGameStarted(g) { // オセロエンジン初期化 o = new Othello(game.settings.map, { - isLlotheo: game.settings.is_llotheo, - canPutEverywhere: game.settings.can_put_everywhere, - loopedBoard: game.settings.looped_board + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard }); // 各マスの価値を計算しておく @@ -141,7 +141,7 @@ function onGameStarted(g) { return count >= 4 ? 1 : 0; }); - botColor = game.user1_id == id && game.black == 1 || game.user2_id == id && game.black == 2; + botColor = game.user1Id == id && game.black == 1 || game.user2Id == id && game.black == 2; if (botColor) { think(); @@ -188,7 +188,7 @@ function think() { }); // ロセオならスコアを反転 - if (game.settings.is_llotheo) score = -score; + if (game.settings.isLlotheo) score = -score; // 接待ならスコアを反転 if (isSettai) score = -score; @@ -234,7 +234,7 @@ function think() { let score; - if (game.settings.is_llotheo) { + if (game.settings.isLlotheo) { // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); } else { @@ -317,7 +317,7 @@ function think() { let score; - if (game.settings.is_llotheo) { + if (game.settings.isLlotheo) { // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); } else { diff --git a/src/server/common/othello/ai/front.ts b/src/server/common/othello/ai/front.ts index af0b748fc0..fb7a9be131 100644 --- a/src/server/common/othello/ai/front.ts +++ b/src/server/common/othello/ai/front.ts @@ -48,12 +48,12 @@ homeStream.on('message', message => { if (msg.type == 'mention' || msg.type == 'reply') { const post = msg.body; - if (post.user_id == id) return; + if (post.userId == id) return; // リアクションする request.post(`${conf.api_url}/posts/reactions/create`, { json: { i, - post_id: post.id, + postId: post.id, reaction: 'love' } }); @@ -62,12 +62,12 @@ homeStream.on('message', message => { if (post.text.indexOf('オセロ') > -1) { request.post(`${conf.api_url}/posts/create`, { json: { i, - reply_id: post.id, + replyId: post.id, text: '良いですよ~' } }); - invite(post.user_id); + invite(post.userId); } } } @@ -79,12 +79,12 @@ homeStream.on('message', message => { if (message.text.indexOf('オセロ') > -1) { request.post(`${conf.api_url}/messaging/messages/create`, { json: { i, - user_id: message.user_id, + userId: message.userId, text: '良いですよ~' } }); - invite(message.user_id); + invite(message.userId); } } } @@ -94,7 +94,7 @@ homeStream.on('message', message => { function invite(userId) { request.post(`${conf.api_url}/othello/match`, { json: { i, - user_id: userId + userId: userId } }); } @@ -225,7 +225,7 @@ async function onInviteMe(inviter) { const game = await request.post(`${conf.api_url}/othello/match`, { json: { i, - user_id: inviter.id + userId: inviter.id } }); diff --git a/src/server/common/user/get-summary.ts b/src/server/common/user/get-summary.ts index f9b7125e30..b314a5cefb 100644 --- a/src/server/common/user/get-summary.ts +++ b/src/server/common/user/get-summary.ts @@ -7,7 +7,7 @@ import getAcct from './get-acct'; */ export default function(user: IUser): string { let string = `${user.name} (@${getAcct(user)})\n` + - `${user.posts_count}投稿、${user.following_count}フォロー、${user.followers_count}フォロワー\n`; + `${user.postsCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`; if (user.host === null) { const account = user.account as ILocalAccount; diff --git a/src/server/web/app/auth/views/form.vue b/src/server/web/app/auth/views/form.vue index d86ed58b38..9d9e8cdb1b 100644 --- a/src/server/web/app/auth/views/form.vue +++ b/src/server/web/app/auth/views/form.vue @@ -7,7 +7,7 @@

{{ app.name }}

-

{{ app.name_id }}

+

{{ app.nameId }}

{{ app.description }}

diff --git a/src/server/web/app/auth/views/index.vue b/src/server/web/app/auth/views/index.vue index 17e5cc6108..e1e1b265e1 100644 --- a/src/server/web/app/auth/views/index.vue +++ b/src/server/web/app/auth/views/index.vue @@ -14,9 +14,9 @@

このアプリがあなたのアカウントにアクセスすることはありません。

-

{{ session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}}

-

アプリケーションに戻っています

-

アプリケーションに戻って、やっていってください。

+

{{ session.app.isAuthorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました' }}

+

アプリケーションに戻っています

+

アプリケーションに戻って、やっていってください。

セッションが存在しません。

@@ -61,7 +61,7 @@ export default Vue.extend({ this.fetching = false; // 既に連携していた場合 - if (this.session.app.is_authorized) { + if (this.session.app.isAuthorized) { this.$root.$data.os.api('auth/accept', { token: this.session.token }).then(() => { @@ -77,8 +77,8 @@ export default Vue.extend({ methods: { accepted() { this.state = 'accepted'; - if (this.session.app.callback_url) { - location.href = this.session.app.callback_url + '?token=' + this.session.token; + if (this.session.app.callbackUrl) { + location.href = this.session.app.callbackUrl + '?token=' + this.session.token; } } } diff --git a/src/server/web/app/ch/tags/channel.tag b/src/server/web/app/ch/tags/channel.tag index dc4b8e1426..2abfb106a5 100644 --- a/src/server/web/app/ch/tags/channel.tag +++ b/src/server/web/app/ch/tags/channel.tag @@ -5,8 +5,8 @@

{ channel.title }

-

このチャンネルをウォッチしています ウォッチ解除

-

このチャンネルをウォッチする

+

このチャンネルをウォッチしています ウォッチ解除

+

このチャンネルをウォッチする