diff options
Diffstat (limited to 'src/api')
97 files changed, 2162 insertions, 1396 deletions
diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts index 2a649788af..1ee455c092 100644 --- a/src/api/common/add-file-to-drive.ts +++ b/src/api/common/add-file-to-drive.ts @@ -5,17 +5,21 @@ import * as stream from 'stream'; import * as mongodb from 'mongodb'; import * as crypto from 'crypto'; -import * as gm from 'gm'; +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 serialize from '../serializers/drive-file'; +import { pack } from '../models/drive-file'; import event, { publishDriveStream } from '../event'; import config from '../../conf'; +const gm = _gm.subClass({ + imageMagick: true +}); + const log = debug('misskey:register-drive-file'); const tmpFile = (): Promise<string> => new Promise((resolve, reject) => { @@ -106,8 +110,8 @@ const addFile = async ( } } - const [properties, folder] = await Promise.all([ - // properties + const [wh, averageColor, folder] = await Promise.all([ + // Width and height (when image) (async () => { // 画像かどうか if (!/^image\/.*$/.test(mime)) { @@ -116,22 +120,49 @@ const addFile = async ( const imageType = mime.split('/')[1]; - // 画像でもPNGかJPEGでないならスキップ - if (imageType != 'png' && imageType != 'jpeg') { + // 画像でもPNGかJPEGかGIFでないならスキップ + if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') { return null; } - // If the file is an image, calculate width and height to save in property + log('calculate image width and height...'); + + // Calculate width and height const g = gm(fs.createReadStream(path), name); const size = await prominence(g).size(); - const properties = { - width: size.width, - height: size.height - }; - log('image width and height is calculated'); + 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(); - return properties; + 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 () => { @@ -181,6 +212,17 @@ const addFile = async ( 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, @@ -224,11 +266,11 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi } rej(new Error('un-compatible file.')); }) - .then(([path, remove]): Promise<any> => new Promise((res, rej) => { + .then(([path, shouldCleanup]): Promise<any> => new Promise((res, rej) => { addFile(user, path, ...args) .then(file => { res(file); - if (remove) { + if (shouldCleanup) { fs.unlink(path, (e) => { if (e) log(e.stack); }); @@ -240,7 +282,7 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi log(`drive file has been created ${file._id}`); resolve(file); - serialize(file).then(serializedFile => { + pack(file).then(serializedFile => { // Publish drive_file_created event event(user._id, 'drive_file_created', serializedFile); publishDriveStream(user._id, 'file_created', serializedFile); diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts index 4b3e6a5d54..ae5669b84c 100644 --- a/src/api/common/notify.ts +++ b/src/api/common/notify.ts @@ -1,7 +1,8 @@ import * as mongo from 'mongodb'; import Notification from '../models/notification'; +import Mute from '../models/mute'; import event from '../event'; -import serialize from '../serializers/notification'; +import { pack } from '../models/notification'; export default ( notifiee: mongo.ObjectID, @@ -26,13 +27,24 @@ export default ( // Publish notification event event(notifiee, 'notification', - await serialize(notification)); + await pack(notification)); // 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する setTimeout(async () => { const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true }); if (!fresh.is_read) { - event(notifiee, 'unread_notification', await serialize(notification)); + //#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/api/endpoints.ts b/src/api/endpoints.ts index 06fb9a64ae..e846381578 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -156,6 +156,21 @@ const endpoints: Endpoint[] = [ 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: { @@ -171,11 +186,13 @@ const endpoints: Endpoint[] = [ }, { name: 'i/change_password', - withCredential: true + withCredential: true, + secure: true }, { name: 'i/regenerate_token', - withCredential: true + withCredential: true, + secure: true }, { name: 'i/pin', @@ -205,6 +222,23 @@ const endpoints: Endpoint[] = [ withCredential: true, kind: 'notification-read' }, + + { + 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, diff --git a/src/api/endpoints/aggregation/posts/reactions.ts b/src/api/endpoints/aggregation/posts/reactions.ts index 2cd4588ae1..790b523be9 100644 --- a/src/api/endpoints/aggregation/posts/reactions.ts +++ b/src/api/endpoints/aggregation/posts/reactions.ts @@ -35,10 +35,13 @@ module.exports = (params) => new Promise(async (res, rej) => { { deleted_at: { $gt: startTime } } ] }, { - _id: false, - post_id: false - }, { - sort: { created_at: -1 } + sort: { + _id: -1 + }, + fields: { + _id: false, + post_id: false + } }); const graph = []; diff --git a/src/api/endpoints/aggregation/users.ts b/src/api/endpoints/aggregation/users.ts index 9eb2d035ec..e38ce92ff9 100644 --- a/src/api/endpoints/aggregation/users.ts +++ b/src/api/endpoints/aggregation/users.ts @@ -17,11 +17,14 @@ module.exports = params => new Promise(async (res, rej) => { const users = await User .find({}, { - _id: false, - created_at: true, - deleted_at: true - }, { - sort: { created_at: -1 } + sort: { + _id: -1 + }, + fields: { + _id: false, + created_at: true, + deleted_at: true + } }); const graph = []; diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts index ca684de02d..0f688792a7 100644 --- a/src/api/endpoints/app/create.ts +++ b/src/api/endpoints/app/create.ts @@ -3,9 +3,7 @@ */ import rndstr from 'rndstr'; import $ from 'cafy'; -import App from '../../models/app'; -import { isValidNameId } from '../../models/app'; -import serialize from '../../serializers/app'; +import App, { isValidNameId, pack } from '../../models/app'; /** * @swagger @@ -106,5 +104,5 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { }); // Response - res(await serialize(app)); + res(await pack(app)); }); diff --git a/src/api/endpoints/app/show.ts b/src/api/endpoints/app/show.ts index 054aab8596..8bc3dda42c 100644 --- a/src/api/endpoints/app/show.ts +++ b/src/api/endpoints/app/show.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import App from '../../models/app'; -import serialize from '../../serializers/app'; +import App, { pack } from '../../models/app'; /** * @swagger @@ -67,7 +66,7 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => } // Send response - res(await serialize(app, user, { + res(await pack(app, user, { includeSecret: isSecure && app.user_id.equals(user._id) })); }); diff --git a/src/api/endpoints/auth/session/show.ts b/src/api/endpoints/auth/session/show.ts index ede8a67634..73ac3185f6 100644 --- a/src/api/endpoints/auth/session/show.ts +++ b/src/api/endpoints/auth/session/show.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import AuthSess from '../../../models/auth-session'; -import serialize from '../../../serializers/auth-session'; +import AuthSess, { pack } from '../../../models/auth-session'; /** * @swagger @@ -67,5 +66,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Response - res(await serialize(session, user)); + res(await pack(session, user)); }); diff --git a/src/api/endpoints/auth/session/userkey.ts b/src/api/endpoints/auth/session/userkey.ts index afd3250b04..fc989bf8c2 100644 --- a/src/api/endpoints/auth/session/userkey.ts +++ b/src/api/endpoints/auth/session/userkey.ts @@ -5,7 +5,7 @@ import $ from 'cafy'; import App from '../../../models/app'; import AuthSess from '../../../models/auth-session'; import AccessToken from '../../../models/access-token'; -import serialize from '../../../serializers/user'; +import { pack } from '../../../models/user'; /** * @swagger @@ -102,7 +102,7 @@ module.exports = (params) => new Promise(async (res, rej) => { // Response res({ access_token: accessToken.token, - user: await serialize(session.user_id, null, { + user: await pack(session.user_id, null, { detail: true }) }); diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts index e10c943896..b9a7d1b788 100644 --- a/src/api/endpoints/channels.ts +++ b/src/api/endpoints/channels.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Channel from '../models/channel'; -import serialize from '../serializers/channel'; +import Channel, { pack } from '../models/channel'; /** * Get all channels @@ -21,13 +20,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 @@ -40,9 +39,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -55,5 +54,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(channels.map(async channel => - await serialize(channel, me)))); + await pack(channel, me)))); }); diff --git a/src/api/endpoints/channels/create.ts b/src/api/endpoints/channels/create.ts index a8d7c29dc1..695b4515b3 100644 --- a/src/api/endpoints/channels/create.ts +++ b/src/api/endpoints/channels/create.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import Channel from '../../models/channel'; import Watching from '../../models/channel-watching'; -import serialize from '../../serializers/channel'; +import { pack } from '../../models/channel'; /** * Create a channel @@ -28,7 +28,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { }); // Response - res(await serialize(channel)); + res(await pack(channel)); // Create Watching await Watching.insert({ diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts index 5c071a124f..d722589c20 100644 --- a/src/api/endpoints/channels/posts.ts +++ b/src/api/endpoints/channels/posts.ts @@ -3,8 +3,7 @@ */ import $ from 'cafy'; import { default as Channel, IChannel } from '../../models/channel'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; +import Post, { pack } from '../../models/post'; /** * Show a posts of a channel @@ -22,13 +21,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 @@ -58,9 +57,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } //#endregion Construct query @@ -74,6 +73,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(posts.map(async (post) => - await serialize(post, user) + await pack(post, user) ))); }); diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts index 8861e54594..332da64675 100644 --- a/src/api/endpoints/channels/show.ts +++ b/src/api/endpoints/channels/show.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import { default as Channel, IChannel } from '../../models/channel'; -import serialize from '../../serializers/channel'; +import Channel, { IChannel, pack } from '../../models/channel'; /** * Show a channel @@ -27,5 +26,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Serialize - res(await serialize(channel, user)); + res(await pack(channel, user)); }); diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts index b2e094775c..89915331ea 100644 --- a/src/api/endpoints/drive/files.ts +++ b/src/api/endpoints/drive/files.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFile from '../../models/drive-file'; -import serialize from '../../serializers/drive-file'; +import DriveFile, { pack } from '../../models/drive-file'; /** * Get drive files @@ -22,13 +21,13 @@ module.exports = async (params, user, app) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) throw 'invalid since_id param'; - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) throw 'invalid max_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 max_id is specified - if (sinceId && maxId) { - throw 'cannot set since_id and max_id'; + // 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 @@ -52,9 +51,9 @@ module.exports = async (params, user, app) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } if (type) { @@ -69,6 +68,6 @@ module.exports = async (params, user, app) => { }); // Serialize - const _files = await Promise.all(files.map(file => serialize(file))); + const _files = await Promise.all(files.map(file => pack(file))); return _files; }; diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts index 7546eca309..96bcace886 100644 --- a/src/api/endpoints/drive/files/create.ts +++ b/src/api/endpoints/drive/files/create.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import { validateFileName } from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; +import { validateFileName, pack } from '../../../models/drive-file'; import create from '../../../common/add-file-to-drive'; /** @@ -38,9 +37,15 @@ module.exports = async (file, params, user): Promise<any> => { const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; if (folderIdErr) throw 'invalid folder_id param'; - // Create file - const driveFile = await create(user, file.path, name, null, folderId); + try { + // Create file + const driveFile = await create(user, file.path, name, null, folderId); - // Serialize - return serialize(driveFile); + // Serialize + return pack(driveFile); + } catch (e) { + console.error(e); + + throw e; + } }; diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts index a1cdf1643e..e026afe936 100644 --- a/src/api/endpoints/drive/files/find.ts +++ b/src/api/endpoints/drive/files/find.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFile from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; +import DriveFile, { pack } from '../../../models/drive-file'; /** * Find a file(s) @@ -31,5 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(files.map(async file => - await serialize(file)))); + await pack(file)))); }); diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts index 3c7cf774f9..21664f7ba4 100644 --- a/src/api/endpoints/drive/files/show.ts +++ b/src/api/endpoints/drive/files/show.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFile from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; +import DriveFile, { pack } from '../../../models/drive-file'; /** * Show a file @@ -29,7 +28,7 @@ module.exports = async (params, user) => { } // Serialize - const _file = await serialize(file, { + const _file = await pack(file, { detail: true }); diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts index f39a420d6e..83da462113 100644 --- a/src/api/endpoints/drive/files/update.ts +++ b/src/api/endpoints/drive/files/update.ts @@ -3,9 +3,7 @@ */ import $ from 'cafy'; import DriveFolder from '../../../models/drive-folder'; -import DriveFile from '../../../models/drive-file'; -import { validateFileName } from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; +import DriveFile, { validateFileName, pack } from '../../../models/drive-file'; import { publishDriveStream } from '../../../event'; /** @@ -67,7 +65,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Serialize - const fileObj = await serialize(file); + const fileObj = await pack(file); // Response res(fileObj); diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts index 519e0bdf65..68428747ef 100644 --- a/src/api/endpoints/drive/files/upload_from_url.ts +++ b/src/api/endpoints/drive/files/upload_from_url.ts @@ -3,8 +3,7 @@ */ import * as URL from 'url'; import $ from 'cafy'; -import { validateFileName } from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; +import { validateFileName, pack } from '../../../models/drive-file'; import create from '../../../common/add-file-to-drive'; import * as debug from 'debug'; import * as tmp from 'tmp'; @@ -63,5 +62,5 @@ module.exports = async (params, user): Promise<any> => { if (e) log(e.stack); }); - return serialize(driveFile); + return pack(driveFile); }; diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts index d49ef0af03..428bde3507 100644 --- a/src/api/endpoints/drive/folders.ts +++ b/src/api/endpoints/drive/folders.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder from '../../models/drive-folder'; -import serialize from '../../serializers/drive-folder'; +import DriveFolder, { pack } from '../../models/drive-folder'; /** * Get drive folders @@ -22,13 +21,13 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 @@ -48,9 +47,9 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -63,5 +62,5 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(folders.map(async folder => - await serialize(folder)))); + await pack(folder)))); }); diff --git a/src/api/endpoints/drive/folders/create.ts b/src/api/endpoints/drive/folders/create.ts index be847b2153..03f396ddc9 100644 --- a/src/api/endpoints/drive/folders/create.ts +++ b/src/api/endpoints/drive/folders/create.ts @@ -2,9 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import { isValidFolderName } from '../../../models/drive-folder'; -import serialize from '../../../serializers/drive-folder'; +import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder'; import { publishDriveStream } from '../../../event'; /** @@ -47,7 +45,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Serialize - const folderObj = await serialize(folder); + const folderObj = await pack(folder); // Response res(folderObj); diff --git a/src/api/endpoints/drive/folders/find.ts b/src/api/endpoints/drive/folders/find.ts index a5eb8e015d..fc84766bc8 100644 --- a/src/api/endpoints/drive/folders/find.ts +++ b/src/api/endpoints/drive/folders/find.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import serialize from '../../../serializers/drive-folder'; +import DriveFolder, { pack } from '../../../models/drive-folder'; /** * Find a folder(s) @@ -30,5 +29,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Serialize - res(await Promise.all(folders.map(folder => serialize(folder)))); + res(await Promise.all(folders.map(folder => pack(folder)))); }); diff --git a/src/api/endpoints/drive/folders/show.ts b/src/api/endpoints/drive/folders/show.ts index 9b1c04ca3c..e07d14d20d 100644 --- a/src/api/endpoints/drive/folders/show.ts +++ b/src/api/endpoints/drive/folders/show.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import serialize from '../../../serializers/drive-folder'; +import DriveFolder, { pack } from '../../../models/drive-folder'; /** * Show a folder @@ -29,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Serialize - res(await serialize(folder, { + res(await pack(folder, { detail: true })); }); diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts index ff673402ab..d3df8bdae5 100644 --- a/src/api/endpoints/drive/folders/update.ts +++ b/src/api/endpoints/drive/folders/update.ts @@ -2,9 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import { isValidFolderName } from '../../../models/drive-folder'; -import serialize from '../../../serializers/drive-folder'; +import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder'; import { publishDriveStream } from '../../../event'; /** @@ -91,7 +89,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Serialize - const folderObj = await serialize(folder); + const folderObj = await pack(folder); // Response res(folderObj); diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts index 7ee255e5d1..8352c7dd4c 100644 --- a/src/api/endpoints/drive/stream.ts +++ b/src/api/endpoints/drive/stream.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFile from '../../models/drive-file'; -import serialize from '../../serializers/drive-file'; +import DriveFile, { pack } from '../../models/drive-file'; /** * Get drive stream @@ -21,13 +20,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 @@ -46,9 +45,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } if (type) { @@ -64,5 +63,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(files.map(async file => - await serialize(file)))); + await pack(file)))); }); diff --git a/src/api/endpoints/following/create.ts b/src/api/endpoints/following/create.ts index b4a2217b16..8e1aa34713 100644 --- a/src/api/endpoints/following/create.ts +++ b/src/api/endpoints/following/create.ts @@ -2,11 +2,10 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; +import User, { pack as packUser } from '../../models/user'; import Following from '../../models/following'; import notify from '../../common/notify'; import event from '../../event'; -import serializeUser from '../../serializers/user'; /** * Follow a user @@ -77,8 +76,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Publish follow event - event(follower._id, 'follow', await serializeUser(followee, follower)); - event(followee._id, 'followed', await serializeUser(follower, followee)); + 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/api/endpoints/following/delete.ts b/src/api/endpoints/following/delete.ts index aa1639ef6c..b68cec09dd 100644 --- a/src/api/endpoints/following/delete.ts +++ b/src/api/endpoints/following/delete.ts @@ -2,10 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; +import User, { pack as packUser } from '../../models/user'; import Following from '../../models/following'; import event from '../../event'; -import serializeUser from '../../serializers/user'; /** * Unfollow a user @@ -78,5 +77,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Publish follow event - event(follower._id, 'unfollow', await serializeUser(followee, follower)); + event(follower._id, 'unfollow', await packUser(followee, follower)); }); diff --git a/src/api/endpoints/i.ts b/src/api/endpoints/i.ts index ae75f11d54..7efdbcd7c9 100644 --- a/src/api/endpoints/i.ts +++ b/src/api/endpoints/i.ts @@ -1,8 +1,7 @@ /** * Module dependencies */ -import User from '../models/user'; -import serialize from '../serializers/user'; +import User, { pack } from '../models/user'; /** * Show myself @@ -15,7 +14,7 @@ import serialize from '../serializers/user'; */ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => { // Serialize - res(await serialize(user, user, { + res(await pack(user, user, { detail: true, includeSecrets: isSecure })); diff --git a/src/api/endpoints/i/2fa/done.ts b/src/api/endpoints/i/2fa/done.ts new file mode 100644 index 0000000000..0b36033bb6 --- /dev/null +++ b/src/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: { + two_factor_secret: user.two_factor_temp_secret, + two_factor_enabled: true + } + }); + + res(); +}); diff --git a/src/api/endpoints/i/2fa/register.ts b/src/api/endpoints/i/2fa/register.ts new file mode 100644 index 0000000000..c2b5037a29 --- /dev/null +++ b/src/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.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/api/endpoints/i/2fa/unregister.ts b/src/api/endpoints/i/2fa/unregister.ts new file mode 100644 index 0000000000..6bee6a26f2 --- /dev/null +++ b/src/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.password); + + if (!same) { + return rej('incorrect password'); + } + + await User.update(user._id, { + $set: { + two_factor_secret: null, + two_factor_enabled: false + } + }); + + res(); +}); diff --git a/src/api/endpoints/i/authorized_apps.ts b/src/api/endpoints/i/authorized_apps.ts index 807ca5b1e7..40ce7a68c8 100644 --- a/src/api/endpoints/i/authorized_apps.ts +++ b/src/api/endpoints/i/authorized_apps.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import AccessToken from '../../models/access-token'; -import serialize from '../../serializers/app'; +import { pack } from '../../models/app'; /** * Get authorized apps of my account @@ -39,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(tokens.map(async token => - await serialize(token.app_id)))); + await pack(token.app_id)))); }); diff --git a/src/api/endpoints/i/favorites.ts b/src/api/endpoints/i/favorites.ts index a66eaa7546..eb464cf0f0 100644 --- a/src/api/endpoints/i/favorites.ts +++ b/src/api/endpoints/i/favorites.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import Favorite from '../../models/favorite'; -import serialize from '../../serializers/post'; +import { pack } from '../../models/post'; /** * Get followers of a user @@ -39,6 +39,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(favorites.map(async favorite => - await serialize(favorite.post) + await pack(favorite.post) ))); }); diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts index 607e0768a4..688039a0dd 100644 --- a/src/api/endpoints/i/notifications.ts +++ b/src/api/endpoints/i/notifications.ts @@ -3,7 +3,8 @@ */ import $ from 'cafy'; import Notification from '../../models/notification'; -import serialize from '../../serializers/notification'; +import Mute from '../../models/mute'; +import { pack } from '../../models/notification'; import getFriends from '../../common/get-friends'; import read from '../../common/read-notification'; @@ -36,17 +37,27 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 + notifiee_id: user._id, + $and: [{ + notifier_id: { + $nin: mute.map(m => m.mutee_id) + } + }] } as any; const sort = { @@ -54,12 +65,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }; if (following) { - // ID list of the user $self and other users who the user follows + // ID list of the user itself and other users who the user follows const followingIds = await getFriends(user._id); - query.notifier_id = { - $in: followingIds - }; + query.$and.push({ + notifier_id: { + $in: followingIds + } + }); } if (type) { @@ -73,9 +86,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -88,7 +101,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(notifications.map(async notification => - await serialize(notification)))); + await pack(notification)))); // Mark as read all if (notifications.length > 0 && markAsRead) { diff --git a/src/api/endpoints/i/pin.ts b/src/api/endpoints/i/pin.ts index a94950d22b..ff546fc2bd 100644 --- a/src/api/endpoints/i/pin.ts +++ b/src/api/endpoints/i/pin.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import User from '../../models/user'; import Post from '../../models/post'; -import serialize from '../../serializers/user'; +import { pack } from '../../models/user'; /** * Pin post @@ -35,7 +35,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { }); // Serialize - const iObj = await serialize(user, user, { + const iObj = await pack(user, user, { detail: true }); diff --git a/src/api/endpoints/i/signin_history.ts b/src/api/endpoints/i/signin_history.ts index 1a6e50c7c8..859e81653d 100644 --- a/src/api/endpoints/i/signin_history.ts +++ b/src/api/endpoints/i/signin_history.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Signin from '../../models/signin'; -import serialize from '../../serializers/signin'; +import Signin, { pack } from '../../models/signin'; /** * Get signin history of my account @@ -21,13 +20,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 = { @@ -43,9 +42,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -58,5 +57,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(history.map(async record => - await serialize(record)))); + await pack(record)))); }); diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts index c484c51a96..7bbbf95900 100644 --- a/src/api/endpoints/i/update.ts +++ b/src/api/endpoints/i/update.ts @@ -2,9 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import { isValidName, isValidDescription, isValidLocation, isValidBirthday } from '../../models/user'; -import serialize from '../../serializers/user'; +import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../models/user'; import event from '../../event'; import config from '../../../conf'; @@ -65,7 +63,7 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re }); // Serialize - const iObj = await serialize(user, user, { + const iObj = await pack(user, user, { detail: true, includeSecrets: isSecure }); diff --git a/src/api/endpoints/messaging/history.ts b/src/api/endpoints/messaging/history.ts index 5f7c9276dd..1683ca7a89 100644 --- a/src/api/endpoints/messaging/history.ts +++ b/src/api/endpoints/messaging/history.ts @@ -3,7 +3,8 @@ */ import $ from 'cafy'; import History from '../../models/messaging-history'; -import serialize from '../../serializers/messaging-message'; +import Mute from '../../models/mute'; +import { pack } from '../../models/messaging-message'; /** * Show messaging history @@ -17,10 +18,18 @@ 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'); + const mute = await Mute.find({ + muter_id: user._id, + deleted_at: { $exists: false } + }); + // Get history const history = await History .find({ - user_id: user._id + user_id: user._id, + partner: { + $nin: mute.map(m => m.mutee_id) + } }, { limit: limit, sort: { @@ -30,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(history.map(async h => - await serialize(h.message, user)))); + await pack(h.message, user)))); }); diff --git a/src/api/endpoints/messaging/messages.ts b/src/api/endpoints/messaging/messages.ts index 7b270924eb..67ba5e9d6d 100644 --- a/src/api/endpoints/messaging/messages.ts +++ b/src/api/endpoints/messaging/messages.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import Message from '../../models/messaging-message'; import User from '../../models/user'; -import serialize from '../../serializers/messaging-message'; +import { pack } from '../../models/messaging-message'; import read from '../../common/read-messaging-message'; /** @@ -44,13 +44,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 = { @@ -72,9 +72,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -87,7 +87,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(messages.map(async message => - await serialize(message, user, { + await pack(message, user, { populateRecipient: false })))); diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts index 3c7689f967..1b8a5f59e6 100644 --- a/src/api/endpoints/messaging/messages/create.ts +++ b/src/api/endpoints/messaging/messages/create.ts @@ -6,8 +6,9 @@ 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 serialize from '../../../serializers/messaging-message'; +import { pack } from '../../../models/messaging-message'; import publishUserStream from '../../../event'; import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event'; import config from '../../../../conf'; @@ -78,7 +79,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Serialize - const messageObj = await serialize(message); + const messageObj = await pack(message); // Reponse res(messageObj); @@ -97,6 +98,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => { 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); } diff --git a/src/api/endpoints/messaging/unread.ts b/src/api/endpoints/messaging/unread.ts index 40bc83fe1c..c4326e1d22 100644 --- a/src/api/endpoints/messaging/unread.ts +++ b/src/api/endpoints/messaging/unread.ts @@ -2,6 +2,7 @@ * Module dependencies */ import Message from '../../models/messaging-message'; +import Mute from '../../models/mute'; /** * Get count of unread messages @@ -11,8 +12,17 @@ import Message from '../../models/messaging-message'; * @return {Promise<any>} */ 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 }); diff --git a/src/api/endpoints/mute/create.ts b/src/api/endpoints/mute/create.ts new file mode 100644 index 0000000000..f44854ab52 --- /dev/null +++ b/src/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<any>} + */ +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, + 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/api/endpoints/mute/delete.ts b/src/api/endpoints/mute/delete.ts new file mode 100644 index 0000000000..d6bff3353a --- /dev/null +++ b/src/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<any>} + */ +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, + 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/api/endpoints/mute/list.ts b/src/api/endpoints/mute/list.ts new file mode 100644 index 0000000000..19e3b157e6 --- /dev/null +++ b/src/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<any>} + */ +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/api/endpoints/my/apps.ts b/src/api/endpoints/my/apps.ts index eb9c758768..b236190506 100644 --- a/src/api/endpoints/my/apps.ts +++ b/src/api/endpoints/my/apps.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import App from '../../models/app'; -import serialize from '../../serializers/app'; +import App, { pack } from '../../models/app'; /** * Get my apps @@ -37,5 +36,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Reply res(await Promise.all(apps.map(async app => - await serialize(app)))); + await pack(app)))); }); diff --git a/src/api/endpoints/notifications/get_unread_count.ts b/src/api/endpoints/notifications/get_unread_count.ts index 9514e78713..845d6b29ce 100644 --- a/src/api/endpoints/notifications/get_unread_count.ts +++ b/src/api/endpoints/notifications/get_unread_count.ts @@ -2,6 +2,7 @@ * Module dependencies */ import Notification from '../../models/notification'; +import Mute from '../../models/mute'; /** * Get count of unread notifications @@ -11,9 +12,18 @@ import Notification from '../../models/notification'; * @return {Promise<any>} */ 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 }); diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts index f6efcc108d..3b29425927 100644 --- a/src/api/endpoints/posts.ts +++ b/src/api/endpoints/posts.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../models/post'; -import serialize from '../serializers/post'; +import Post, { pack } from '../models/post'; /** * Lists all posts @@ -36,13 +35,13 @@ module.exports = (params) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 @@ -55,9 +54,9 @@ module.exports = (params) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -85,5 +84,5 @@ module.exports = (params) => new Promise(async (res, rej) => { }); // Serialize - res(await Promise.all(posts.map(async post => await serialize(post)))); + res(await Promise.all(posts.map(async post => await pack(post)))); }); diff --git a/src/api/endpoints/posts/context.ts b/src/api/endpoints/posts/context.ts index bad59a6bee..5ba3758975 100644 --- a/src/api/endpoints/posts/context.ts +++ b/src/api/endpoints/posts/context.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; +import Post, { pack } from '../../models/post'; /** * Show a context of a post @@ -60,5 +59,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(context.map(async post => - await serialize(post, user)))); + await pack(post, user)))); }); diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts index ae4959dae4..0fa52221f9 100644 --- a/src/api/endpoints/posts/create.ts +++ b/src/api/endpoints/posts/create.ts @@ -8,10 +8,11 @@ import { default as Post, IPost, isValidText } from '../../models/post'; import { default as User, 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 serialize from '../../serializers/post'; +import { pack } from '../../models/post'; import notify from '../../common/notify'; import watch from '../../common/watch-post'; import event, { pushSw, publishChannelStream } from '../../event'; @@ -215,14 +216,20 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { poll: poll, text: text, user_id: user._id, - app_id: app ? app._id : null + app_id: app ? app._id : null, + + // 以下非正規化データ + _reply: reply ? { user_id: reply.user_id } : undefined, + _repost: repost ? { user_id: repost.user_id } : undefined, }); // Serialize - const postObj = await serialize(post); + const postObj = await pack(post); // Reponse - res(postObj); + res({ + created_post: postObj + }); //#region Post processes @@ -234,7 +241,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { const mentions = []; - function addMention(mentionee, reason) { + async function addMention(mentionee, reason) { // Reject if already added if (mentions.some(x => x.equals(mentionee))) return; @@ -243,8 +250,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // Publish event if (!user._id.equals(mentionee)) { - event(mentionee, reason, postObj); - pushSw(mentionee, reason, postObj); + 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); + } } } diff --git a/src/api/endpoints/posts/mentions.ts b/src/api/endpoints/posts/mentions.ts index 0ebe8be503..7127db0ad1 100644 --- a/src/api/endpoints/posts/mentions.ts +++ b/src/api/endpoints/posts/mentions.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import Post from '../../models/post'; import getFriends from '../../common/get-friends'; -import serialize from '../../serializers/post'; +import { pack } from '../../models/post'; /** * Get mentions of myself @@ -27,13 +27,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 @@ -58,9 +58,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -73,6 +73,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(mentions.map(async mention => - await serialize(mention, user) + await pack(mention, user) ))); }); diff --git a/src/api/endpoints/posts/polls/recommendation.ts b/src/api/endpoints/posts/polls/recommendation.ts index 9c92d6cac4..4a3fa3f55e 100644 --- a/src/api/endpoints/posts/polls/recommendation.ts +++ b/src/api/endpoints/posts/polls/recommendation.ts @@ -3,8 +3,7 @@ */ import $ from 'cafy'; import Vote from '../../../models/poll-vote'; -import Post from '../../../models/post'; -import serialize from '../../../serializers/post'; +import Post, { pack } from '../../../models/post'; /** * Get recommended polls @@ -56,5 +55,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(posts.map(async post => - await serialize(post, user, { detail: true })))); + await pack(post, user, { detail: true })))); }); diff --git a/src/api/endpoints/posts/reactions.ts b/src/api/endpoints/posts/reactions.ts index eab5d9b258..feb140ab41 100644 --- a/src/api/endpoints/posts/reactions.ts +++ b/src/api/endpoints/posts/reactions.ts @@ -3,8 +3,7 @@ */ import $ from 'cafy'; import Post from '../../models/post'; -import Reaction from '../../models/post-reaction'; -import serialize from '../../serializers/post-reaction'; +import Reaction, { pack } from '../../models/post-reaction'; /** * Show reactions of a post @@ -54,5 +53,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(reactions.map(async reaction => - await serialize(reaction, user)))); + await pack(reaction, user)))); }); diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts index d537463dfe..0b0e0e294d 100644 --- a/src/api/endpoints/posts/reactions/create.ts +++ b/src/api/endpoints/posts/reactions/create.ts @@ -3,13 +3,12 @@ */ import $ from 'cafy'; import Reaction from '../../../models/post-reaction'; -import Post from '../../../models/post'; +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'; -import serializePost from '../../../serializers/post'; -import serializeUser from '../../../serializers/user'; /** * React to a post @@ -90,8 +89,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); pushSw(post.user_id, 'reaction', { - user: await serializeUser(user, post.user_id), - post: await serializePost(post, post.user_id), + user: await packUser(user, post.user_id), + post: await packPost(post, post.user_id), reaction: reaction }); diff --git a/src/api/endpoints/posts/replies.ts b/src/api/endpoints/posts/replies.ts index 3fd6a46769..613c4fa24c 100644 --- a/src/api/endpoints/posts/replies.ts +++ b/src/api/endpoints/posts/replies.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; +import Post, { pack } from '../../models/post'; /** * Show a replies of a post @@ -50,5 +49,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(replies.map(async post => - await serialize(post, user)))); + await pack(post, user)))); }); diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts index b701ff7574..89ab0e3d55 100644 --- a/src/api/endpoints/posts/reposts.ts +++ b/src/api/endpoints/posts/reposts.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; +import Post, { pack } from '../../models/post'; /** * Show a reposts of a post @@ -25,13 +24,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 @@ -55,9 +54,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -70,5 +69,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(reposts.map(async post => - await serialize(post, user)))); + await pack(post, user)))); }); diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts index b434f64342..6e26f55390 100644 --- a/src/api/endpoints/posts/search.ts +++ b/src/api/endpoints/posts/search.ts @@ -1,12 +1,13 @@ /** * Module dependencies */ -import * as mongo from 'mongodb'; import $ from 'cafy'; const escapeRegexp = require('escape-regexp'); import Post from '../../models/post'; -import serialize from '../../serializers/post'; -import config from '../../../conf'; +import User from '../../models/user'; +import Mute from '../../models/mute'; +import getFriends from '../../common/get-friends'; +import { pack } from '../../models/post'; /** * Search a post @@ -16,104 +17,339 @@ import config from '../../../conf'; * @return {Promise<any>} */ 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 '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 'max' parameter - const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$; - if (maxErr) return rej('invalid max 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); + } - // If Elasticsearch is available, search by $ - // If not, search by MongoDB - (config.elasticsearch.enable ? byElasticsearch : byNative) - (res, rej, me, query, offset, max); + 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); }); -// Search by MongoDB -async function byNative(res, rej, me, query, offset, max) { - const escapedQuery = escapeRegexp(query); +async function search( + res, rej, me, text, includeUserIds, excludeUserIds, following, + mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) { - // Search posts - const posts = await Post - .find({ - text: new RegExp(escapedQuery) - }, { - sort: { - _id: -1 - }, - limit: max, - skip: offset - }); + let q: any = { + $and: [] + }; - // Serialize - res(await Promise.all(posts.map(async post => - await serialize(post, me)))); -} + const push = x => q.$and.push(x); -// Search by Elasticsearch -async function byElasticsearch(res, rej, me, query, offset, max) { - const es = require('../../db/elasticsearch'); + if (text) { + // 完全一致検索 + if (/"""(.+?)"""/.test(text)) { + const x = text.match(/"""(.+?)"""/)[1]; + push({ + text: x + }); + } else { + push({ + $and: text.split(' ').map(x => ({ + // キーワードが-で始まる場合そのキーワードを除外する + text: x[0] == '-' ? { + $not: new RegExp(escapeRegexp(x.substr(1))) + } : new RegExp(escapeRegexp(x)) + })) + }); + } + } - es.search({ - index: 'misskey', - type: 'post', - body: { - size: max, - from: offset, - query: { - simple_query_string: { - fields: ['text'], - query: query, - default_operator: 'and' - } - }, - sort: [ - { _doc: 'desc' } - ], - highlight: { - pre_tags: ['<mark>'], - post_tags: ['</mark>'], - encoder: 'html', - fields: { - text: {} - } + 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; } - }, async (error, response) => { - if (error) { - console.error(error); - return res(500); - } + } - if (response.hits.total === 0) { - return res([]); + if (reply != null) { + if (reply) { + push({ + reply_id: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + reply_id: { + $exists: false + } + }, { + reply_id: null + }] + }); } + } - const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id)); + if (repost != null) { + if (repost) { + push({ + repost_id: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + repost_id: { + $exists: false + } + }, { + repost_id: null + }] + }); + } + } - // Fetch found posts - const posts = await Post - .find({ - _id: { - $in: hits + if (media != null) { + if (media) { + push({ + media_ids: { + $exists: true, + $ne: null } - }, { - sort: { - _id: -1 + }); + } 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 = {}; + } - posts.map(post => { - post._highlight = response.hits.hits.filter(hit => post._id.equals(hit._id))[0].highlight.text[0]; + // 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 serialize(post, me)))); - }); + // Serialize + res(await Promise.all(posts.map(async post => + await pack(post, me)))); } diff --git a/src/api/endpoints/posts/show.ts b/src/api/endpoints/posts/show.ts index 5bfe4f6605..3839490597 100644 --- a/src/api/endpoints/posts/show.ts +++ b/src/api/endpoints/posts/show.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; +import Post, { pack } from '../../models/post'; /** * Show a post @@ -27,7 +26,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Serialize - res(await serialize(post, user, { + res(await pack(post, user, { detail: true })); }); diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts index 0d08b95463..c41cfdb8bd 100644 --- a/src/api/endpoints/posts/timeline.ts +++ b/src/api/endpoints/posts/timeline.ts @@ -4,9 +4,10 @@ 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 serialize from '../../serializers/post'; +import { pack } from '../../models/post'; /** * Get timeline of myself @@ -25,32 +26,40 @@ module.exports = async (params, user, app) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) throw 'invalid since_id param'; - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) throw 'invalid max_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 'max_date' parameter - const [maxDate, maxDateErr] = $(params.max_date).optional.number().$; - if (maxDateErr) throw 'invalid max_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, max_id, since_date, max_date specified - if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) { - throw 'only one of since_id, max_id, since_date, max_date can be specified'; + // 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 } = await rap({ + 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)) + }).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 @@ -77,7 +86,17 @@ module.exports = async (params, user, app) => { channel_id: { $in: watchingChannelIds } - }] + }], + // mute + user_id: { + $nin: mutedUserIds + }, + '_reply.user_id': { + $nin: mutedUserIds + }, + '_repost.user_id': { + $nin: mutedUserIds + }, } as any; if (sinceId) { @@ -85,18 +104,18 @@ module.exports = async (params, user, app) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } else if (sinceDate) { sort._id = 1; query.created_at = { $gt: new Date(sinceDate) }; - } else if (maxDate) { + } else if (untilDate) { query.created_at = { - $lt: new Date(maxDate) + $lt: new Date(untilDate) }; } //#endregion @@ -109,5 +128,5 @@ module.exports = async (params, user, app) => { }); // Serialize - return await Promise.all(timeline.map(post => serialize(post, user))); + return await Promise.all(timeline.map(post => pack(post, user))); }; diff --git a/src/api/endpoints/posts/trend.ts b/src/api/endpoints/posts/trend.ts index 64a195dff1..caded92bf5 100644 --- a/src/api/endpoints/posts/trend.ts +++ b/src/api/endpoints/posts/trend.ts @@ -3,8 +3,7 @@ */ const ms = require('ms'); import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; +import Post, { pack } from '../../models/post'; /** * Get trend posts @@ -76,5 +75,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(posts.map(async post => - await serialize(post, user, { detail: true })))); + await pack(post, user, { detail: true })))); }); diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts index 134f262fb1..095b9fe40d 100644 --- a/src/api/endpoints/users.ts +++ b/src/api/endpoints/users.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../models/user'; -import serialize from '../serializers/user'; +import User, { pack } from '../models/user'; /** * Lists all users @@ -21,13 +20,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // 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 @@ -40,9 +39,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -55,5 +54,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(users.map(async user => - await serialize(user, me)))); + await pack(user, me)))); }); diff --git a/src/api/endpoints/users/followers.ts b/src/api/endpoints/users/followers.ts index 4905323ba5..b0fb83c683 100644 --- a/src/api/endpoints/users/followers.ts +++ b/src/api/endpoints/users/followers.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import User from '../../models/user'; import Following from '../../models/following'; -import serialize from '../../serializers/user'; +import { pack } from '../../models/user'; import getFriends from '../../common/get-friends'; /** @@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize const users = await Promise.all(following.map(async f => - await serialize(f.follower_id, me, { detail: true }))); + await pack(f.follower_id, me, { detail: true }))); // Response res({ diff --git a/src/api/endpoints/users/following.ts b/src/api/endpoints/users/following.ts index dc2ff49bbe..8e88431e92 100644 --- a/src/api/endpoints/users/following.ts +++ b/src/api/endpoints/users/following.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import User from '../../models/user'; import Following from '../../models/following'; -import serialize from '../../serializers/user'; +import { pack } from '../../models/user'; import getFriends from '../../common/get-friends'; /** @@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize const users = await Promise.all(following.map(async f => - await serialize(f.followee_id, me, { detail: true }))); + await pack(f.followee_id, me, { detail: true }))); // Response res({ diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts index a8add623d4..87f4f77a5b 100644 --- a/src/api/endpoints/users/get_frequently_replied_users.ts +++ b/src/api/endpoints/users/get_frequently_replied_users.ts @@ -3,8 +3,7 @@ */ import $ from 'cafy'; import Post from '../../models/post'; -import User from '../../models/user'; -import serialize from '../../serializers/user'; +import User, { pack } from '../../models/user'; module.exports = (params, me) => new Promise(async (res, rej) => { // Get 'user_id' parameter @@ -91,7 +90,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Make replies object (includes weights) const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ - user: await serialize(user, me, { detail: true }), + user: await pack(user, me, { detail: true }), weight: repliedUsers[user] / peak }))); diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts index fe821cf17a..0c8bceee3d 100644 --- a/src/api/endpoints/users/posts.ts +++ b/src/api/endpoints/users/posts.ts @@ -2,9 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; +import Post, { pack } from '../../models/post'; import User from '../../models/user'; -import serialize from '../../serializers/post'; /** * Get posts of a user @@ -42,21 +41,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; if (sinceIdErr) return rej('invalid since_id param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_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 'max_date' parameter - const [maxDate, maxDateErr] = $(params.max_date).optional.number().$; - if (maxDateErr) throw 'invalid max_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, max_id, since_date, max_date specified - if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) { - throw 'only one of since_id, max_id, since_date, max_date can be specified'; + // 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 @@ -88,18 +87,18 @@ module.exports = (params, me) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } else if (sinceDate) { sort._id = 1; query.created_at = { $gt: new Date(sinceDate) }; - } else if (maxDate) { + } else if (untilDate) { query.created_at = { - $lt: new Date(maxDate) + $lt: new Date(untilDate) }; } @@ -124,6 +123,6 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(posts.map(async (post) => - await serialize(post, me) + await pack(post, me) ))); }); diff --git a/src/api/endpoints/users/recommendation.ts b/src/api/endpoints/users/recommendation.ts index 731d68a7b1..736233b340 100644 --- a/src/api/endpoints/users/recommendation.ts +++ b/src/api/endpoints/users/recommendation.ts @@ -3,8 +3,7 @@ */ const ms = require('ms'); import $ from 'cafy'; -import User from '../../models/user'; -import serialize from '../../serializers/user'; +import User, { pack } from '../../models/user'; import getFriends from '../../common/get-friends'; /** @@ -44,5 +43,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(users.map(async user => - await serialize(user, me, { detail: true })))); + await pack(user, me, { detail: true })))); }); diff --git a/src/api/endpoints/users/search.ts b/src/api/endpoints/users/search.ts index 73a5db47e2..39e2ff9890 100644 --- a/src/api/endpoints/users/search.ts +++ b/src/api/endpoints/users/search.ts @@ -3,8 +3,7 @@ */ import * as mongo from 'mongodb'; import $ from 'cafy'; -import User from '../../models/user'; -import serialize from '../../serializers/user'; +import User, { pack } from '../../models/user'; import config from '../../../conf'; const escapeRegexp = require('escape-regexp'); @@ -52,7 +51,7 @@ async function byNative(res, rej, me, query, offset, max) { // Serialize res(await Promise.all(users.map(async user => - await serialize(user, me, { detail: true })))); + await pack(user, me, { detail: true })))); } // Search by Elasticsearch @@ -94,6 +93,6 @@ async function byElasticsearch(res, rej, me, query, offset, max) { // Serialize res(await Promise.all(users.map(async user => - await serialize(user, me, { detail: true })))); + await pack(user, me, { detail: true })))); }); } diff --git a/src/api/endpoints/users/search_by_username.ts b/src/api/endpoints/users/search_by_username.ts index 7f2f42f0a6..9c5e1905aa 100644 --- a/src/api/endpoints/users/search_by_username.ts +++ b/src/api/endpoints/users/search_by_username.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import serialize from '../../serializers/user'; +import User, { pack } from '../../models/user'; /** * Search a user by username @@ -35,5 +34,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(users.map(async user => - await serialize(user, me, { detail: true })))); + await pack(user, me, { detail: true })))); }); diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts index 8e74b0fe3f..7aea59296a 100644 --- a/src/api/endpoints/users/show.ts +++ b/src/api/endpoints/users/show.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import serialize from '../../serializers/user'; +import User, { pack } from '../../models/user'; /** * Show a user @@ -41,7 +40,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { } // Send response - res(await serialize(user, me, { + res(await pack(user, me, { detail: true })); }); diff --git a/src/api/models/app.ts b/src/api/models/app.ts index 68f2f448b0..34e9867db7 100644 --- a/src/api/models/app.ts +++ b/src/api/models/app.ts @@ -1,13 +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 collection = db.get('apps'); +const App = db.get<IApp>('apps'); +App.createIndex('name_id'); +App.createIndex('name_id_lower'); +App.createIndex('secret'); +export default App; -(collection as any).createIndex('name_id'); // fuck type definition -(collection as any).createIndex('name_id_lower'); // fuck type definition -(collection as any).createIndex('secret'); // fuck type definition - -export default collection as any; // fuck type definition +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<any>} + */ +export const pack = ( + app: any, + me?: any, + options?: { + includeSecret?: boolean, + includeProfileImageIds?: boolean + } +) => new Promise<any>(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/api/models/auth-session.ts b/src/api/models/auth-session.ts index b264a133e9..997ec61c20 100644 --- a/src/api/models/auth-session.ts +++ b/src/api/models/auth-session.ts @@ -1,3 +1,45 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); import db from '../../db/mongodb'; +import { pack as packApp } from './app'; -export default db.get('auth_sessions') as any; // fuck type definition +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<any>} + */ +export const pack = ( + session: any, + me?: any +) => new Promise<any>(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/api/models/channel.ts b/src/api/models/channel.ts index c80e84dbc8..815d53593c 100644 --- a/src/api/models/channel.ts +++ b/src/api/models/channel.ts @@ -1,9 +1,11 @@ import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import { IUser } from './user'; +import Watching from './channel-watching'; import db from '../../db/mongodb'; -const collection = db.get('channels'); - -export default collection as any; // fuck type definition +const Channel = db.get<IChannel>('channels'); +export default Channel; export type IChannel = { _id: mongo.ObjectID; @@ -12,3 +14,61 @@ export type IChannel = { 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<any>(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/api/models/drive-file.ts b/src/api/models/drive-file.ts index 802ee5a5fe..2a46d8dc4d 100644 --- a/src/api/models/drive-file.ts +++ b/src/api/models/drive-file.ts @@ -1,9 +1,12 @@ 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 collection = monkDb.get('drive_files.files'); +const DriveFile = monkDb.get<IDriveFile>('drive_files.files'); -export default collection as any; // fuck type definition +export default DriveFile; const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => { const db = await nativeDbConn(); @@ -15,6 +18,19 @@ const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => { 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) && @@ -24,3 +40,74 @@ export function validateFileName(name: string): boolean { (name.indexOf('..') === -1) ); } + +/** + * Pack a drive file for API response + * + * @param {any} file + * @param {any} options? + * @return {Promise<any>} + */ +export const pack = ( + file: any, + options?: { + detail: boolean + } +) => new Promise<any>(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/api/models/drive-folder.ts b/src/api/models/drive-folder.ts index f81ffe855d..54b45049b9 100644 --- a/src/api/models/drive-folder.ts +++ b/src/api/models/drive-folder.ts @@ -1,6 +1,18 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); import db from '../../db/mongodb'; +import DriveFile from './drive-file'; -export default db.get('drive_folders') as any; // fuck type definition +const DriveFolder = db.get<IDriveFolder>('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 ( @@ -8,3 +20,58 @@ export function isValidFolderName(name: string): boolean { (name.length <= 200) ); } + +/** + * Pack a drive folder for API response + * + * @param {any} folder + * @param {any} options? + * @return {Promise<any>} + */ +export const pack = ( + folder: any, + options?: { + detail: boolean + } +) => new Promise<any>(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/api/models/messaging-message.ts b/src/api/models/messaging-message.ts index 18afa57e44..90cf1cd71c 100644 --- a/src/api/models/messaging-message.ts +++ b/src/api/models/messaging-message.ts @@ -1,12 +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'; -export default db.get('messaging_messages') as any; // fuck type definition +const MessagingMessage = db.get<IMessagingMessage>('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<any>} + */ +export const pack = ( + message: any, + me?: any, + options?: { + populateRecipient: boolean + } +) => new Promise<any>(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) { + // 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/api/models/mute.ts b/src/api/models/mute.ts new file mode 100644 index 0000000000..16018b82f7 --- /dev/null +++ b/src/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/api/models/notification.ts b/src/api/models/notification.ts index e3dc6c70a3..fa7049d312 100644 --- a/src/api/models/notification.ts +++ b/src/api/models/notification.ts @@ -1,8 +1,11 @@ import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); import db from '../../db/mongodb'; -import { IUser } from './user'; +import { IUser, pack as packUser } from './user'; +import { pack as packPost } from './post'; -export default db.get('notifications') as any; // fuck type definition +const Notification = db.get<INotification>('notifications'); +export default Notification; export interface INotification { _id: mongo.ObjectID; @@ -45,3 +48,60 @@ export interface INotification { */ is_read: Boolean; } + +/** + * Pack a notification for API response + * + * @param {any} notification + * @return {Promise<any>} + */ +export const pack = (notification: any) => new Promise<any>(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/api/models/post-reaction.ts b/src/api/models/post-reaction.ts index 282ae5bd21..639a70e006 100644 --- a/src/api/models/post-reaction.ts +++ b/src/api/models/post-reaction.ts @@ -1,3 +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'; -export default db.get('post_reactions') as any; // fuck type definition +const PostReaction = db.get<IPostReaction>('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<any>} + */ +export const pack = ( + reaction: any, + me?: any +) => new Promise<any>(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/api/models/post.ts b/src/api/models/post.ts index 7584ce182d..0bbacebf66 100644 --- a/src/api/models/post.ts +++ b/src/api/models/post.ts @@ -1,8 +1,18 @@ 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<IPost>('posts'); -export default db.get('posts') as any; // fuck type definition +export default Post; export function isValidText(text: string): boolean { return text.length <= 1000 && text.trim() != ''; @@ -15,8 +25,185 @@ export type IPost = { media_ids: mongo.ObjectID[]; reply_id: mongo.ObjectID; repost_id: mongo.ObjectID; - poll: {}; // todo + poll: any; // todo text: string; user_id: mongo.ObjectID; app_id: mongo.ObjectID; + category: string; + is_category_verified: boolean; +}; + +/** + * 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/api/models/signin.ts b/src/api/models/signin.ts index 385a348f2e..262c8707ed 100644 --- a/src/api/models/signin.ts +++ b/src/api/models/signin.ts @@ -1,3 +1,29 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); import db from '../../db/mongodb'; -export default db.get('signin') as any; // fuck type definition +const Signin = db.get<ISignin>('signin'); +export default Signin; + +export interface ISignin { + _id: mongo.ObjectID; +} + +/** + * Pack a signin record for API response + * + * @param {any} record + * @return {Promise<any>} + */ +export const pack = ( + record: any +) => new Promise<any>(async (resolve, reject) => { + + const _record = deepcopy(record); + + // Rename _id to id + _record.id = _record._id; + delete _record._id; + + resolve(_record); +}); diff --git a/src/api/models/user.ts b/src/api/models/user.ts index b2f3af09fa..e92f244dd0 100644 --- a/src/api/models/user.ts +++ b/src/api/models/user.ts @@ -1,14 +1,19 @@ import * as mongo from 'mongodb'; - +import deepcopy = require('deepcopy'); +import rap from '@prezzemolo/rap'; import db from '../../db/mongodb'; -import { IPost } from './post'; +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 collection = db.get('users'); +const User = db.get<IUser>('users'); -(collection as any).createIndex('username'); // fuck type definition -(collection as any).createIndex('token'); // fuck type definition +User.createIndex('username'); +User.createIndex('token'); -export default collection as any; // fuck type definition +export default User; export function validateUsername(username: string): boolean { return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username); @@ -37,6 +42,7 @@ export function isValidBirthday(birthday: string): boolean { export type IUser = { _id: mongo.ObjectID; created_at: Date; + deleted_at: Date; email: string; followers_count: number; following_count: number; @@ -72,6 +78,8 @@ export type IUser = { is_pro: boolean; is_suspended: boolean; keywords: string[]; + two_factor_secret: string; + two_factor_enabled: boolean; }; export function init(user): IUser { @@ -81,3 +89,182 @@ export function init(user): IUser { 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<any>(async (resolve, reject) => { + + const opts = Object.assign({ + detail: false, + includeSecrets: false + }, options); + + let _user: any; + + const fields = opts.detail ? { + settings: false + } : { + settings: false, + client_settings: false, + profile: false, + keywords: false, + 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; + + // Remove private properties + delete _user.password; + delete _user.token; + delete _user.two_factor_temp_secret; + delete _user.two_factor_secret; + delete _user.username_lower; + if (_user.twitter) { + delete _user.twitter.access_token; + delete _user.twitter.access_token_secret; + } + delete _user.line; + + // Visible via only the official client + if (!opts.includeSecrets) { + delete _user.email; + delete _user.client_settings; + } + + if (!opts.detail) { + delete _user.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); +}); + +/* +function img(url) { + return { + thumbnail: { + large: `${url}`, + medium: '', + small: '' + } + }; +} +*/ diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts index 0ebf8d6aa1..b49d25d99a 100644 --- a/src/api/private/signin.ts +++ b/src/api/private/signin.ts @@ -1,16 +1,19 @@ import * as express from 'express'; import * as bcrypt from 'bcryptjs'; +import * as speakeasy from 'speakeasy'; import { default as User, IUser } from '../models/user'; -import Signin from '../models/signin'; -import serialize from '../serializers/signin'; +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); @@ -22,6 +25,11 @@ export default async (req: express.Request, res: express.Response) => { return; } + if (token != null && typeof token != 'string') { + res.sendStatus(400); + return; + } + // Fetch user const user: IUser = await User.findOne({ username_lower: username.toLowerCase() @@ -43,7 +51,23 @@ export default async (req: express.Request, res: express.Response) => { const same = await bcrypt.compare(password, user.password); if (same) { - signin(res, user, false); + if (user.two_factor_enabled) { + const verified = (speakeasy as any).totp.verify({ + secret: user.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' @@ -60,5 +84,5 @@ export default async (req: express.Request, res: express.Response) => { }); // Publish signin event - event(user._id, 'signin', await serialize(record)); + event(user._id, 'signin', await pack(record)); }; diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts index 466c6a489f..8efdb6db47 100644 --- a/src/api/private/signup.ts +++ b/src/api/private/signup.ts @@ -2,9 +2,7 @@ import * as uuid from 'uuid'; import * as express from 'express'; import * as bcrypt from 'bcryptjs'; import recaptcha = require('recaptcha-promise'); -import { default as User, IUser } from '../models/user'; -import { validateUsername, validatePassword } from '../models/user'; -import serialize from '../serializers/user'; +import User, { IUser, validateUsername, validatePassword, pack } from '../models/user'; import generateUserToken from '../common/generate-native-user-token'; import config from '../../conf'; @@ -142,7 +140,7 @@ export default async (req: express.Request, res: express.Response) => { }); // Response - res.send(await serialize(account)); + res.send(await pack(account)); // Create search index if (config.elasticsearch.enable) { diff --git a/src/api/serializers/app.ts b/src/api/serializers/app.ts deleted file mode 100644 index 9d1c46dca4..0000000000 --- a/src/api/serializers/app.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import App from '../models/app'; -import AccessToken from '../models/access-token'; -import config from '../../conf'; - -/** - * Serialize an app - * - * @param {any} app - * @param {any} me? - * @param {any} options? - * @return {Promise<any>} - */ -export default ( - app: any, - me?: any, - options?: { - includeSecret?: boolean, - includeProfileImageIds?: boolean - } -) => new Promise<any>(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/api/serializers/auth-session.ts b/src/api/serializers/auth-session.ts deleted file mode 100644 index a9acf1243a..0000000000 --- a/src/api/serializers/auth-session.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import serializeApp from './app'; - -/** - * Serialize an auth session - * - * @param {any} session - * @param {any} me? - * @return {Promise<any>} - */ -export default ( - session: any, - me?: any -) => new Promise<any>(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 serializeApp(_session.app_id, me); - - resolve(_session); -}); diff --git a/src/api/serializers/channel.ts b/src/api/serializers/channel.ts deleted file mode 100644 index 3cba39aa16..0000000000 --- a/src/api/serializers/channel.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import { IUser } from '../models/user'; -import { default as Channel, IChannel } from '../models/channel'; -import Watching from '../models/channel-watching'; - -/** - * Serialize a channel - * - * @param channel target - * @param me? serializee - * @return response - */ -export default ( - channel: string | mongo.ObjectID | IChannel, - me?: string | mongo.ObjectID | IUser -) => new Promise<any>(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/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts deleted file mode 100644 index dcdaa01fab..0000000000 --- a/src/api/serializers/drive-file.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import DriveFile from '../models/drive-file'; -import serializeDriveFolder from './drive-folder'; -import serializeDriveTag from './drive-tag'; -import deepcopy = require('deepcopy'); -import config from '../../conf'; - -/** - * Serialize a drive file - * - * @param {any} file - * @param {any} options? - * @return {Promise<any>} - */ -export default ( - file: any, - options?: { - detail: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = Object.assign({ - detail: false - }, options); - - let _file: any; - - // Populate the file if 'file' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(file)) { - _file = await DriveFile.findOne({ - _id: file - }); - } else if (typeof file === 'string') { - _file = await DriveFile.findOne({ - _id: new mongo.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 (opts.detail && _target.folder_id) { - // Populate folder - _target.folder = await serializeDriveFolder(_target.folder_id, { - detail: true - }); - } - - if (opts.detail && _target.tags) { - // Populate tags - _target.tags = await _target.tags.map(async (tag: any) => - await serializeDriveTag(tag) - ); - } - - resolve(_target); -}); diff --git a/src/api/serializers/drive-folder.ts b/src/api/serializers/drive-folder.ts deleted file mode 100644 index 6ebf454a28..0000000000 --- a/src/api/serializers/drive-folder.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import DriveFolder from '../models/drive-folder'; -import DriveFile from '../models/drive-file'; -import deepcopy = require('deepcopy'); - -/** - * Serialize a drive folder - * - * @param {any} folder - * @param {any} options? - * @return {Promise<any>} - */ -const self = ( - folder: any, - options?: { - detail: boolean - } -) => new Promise<any>(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 self(_folder.parent_id, { - detail: true - }); - } - - resolve(_folder); -}); - -export default self; diff --git a/src/api/serializers/drive-tag.ts b/src/api/serializers/drive-tag.ts deleted file mode 100644 index 2f152381bd..0000000000 --- a/src/api/serializers/drive-tag.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import DriveTag from '../models/drive-tag'; -import deepcopy = require('deepcopy'); - -/** - * Serialize a drive tag - * - * @param {any} tag - * @return {Promise<any>} - */ -const self = ( - tag: any -) => new Promise<any>(async (resolve, reject) => { - let _tag: any; - - // Populate the tag if 'tag' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(tag)) { - _tag = await DriveTag.findOne({ _id: tag }); - } else if (typeof tag === 'string') { - _tag = await DriveTag.findOne({ _id: new mongo.ObjectID(tag) }); - } else { - _tag = deepcopy(tag); - } - - // Rename _id to id - _tag.id = _tag._id; - delete _tag._id; - - resolve(_tag); -}); - -export default self; diff --git a/src/api/serializers/messaging-message.ts b/src/api/serializers/messaging-message.ts deleted file mode 100644 index 4ab95e42a3..0000000000 --- a/src/api/serializers/messaging-message.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import Message from '../models/messaging-message'; -import serializeUser from './user'; -import serializeDriveFile from './drive-file'; -import parse from '../common/text'; - -/** - * Serialize a message - * - * @param {any} message - * @param {any} me? - * @param {any} options? - * @return {Promise<any>} - */ -export default ( - message: any, - me?: any, - options?: { - populateRecipient: boolean - } -) => new Promise<any>(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 Message.findOne({ - _id: message - }); - } else if (typeof message === 'string') { - _message = await Message.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 serializeUser(_message.user_id, me); - - if (_message.file) { - // Populate file - _message.file = await serializeDriveFile(_message.file_id); - } - - if (opts.populateRecipient) { - // Populate recipient - _message.recipient = await serializeUser(_message.recipient_id, me); - } - - resolve(_message); -}); diff --git a/src/api/serializers/notification.ts b/src/api/serializers/notification.ts deleted file mode 100644 index ac919dc8b0..0000000000 --- a/src/api/serializers/notification.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import Notification from '../models/notification'; -import serializeUser from './user'; -import serializePost from './post'; -import deepcopy = require('deepcopy'); - -/** - * Serialize a notification - * - * @param {any} notification - * @return {Promise<any>} - */ -export default (notification: any) => new Promise<any>(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 serializeUser(_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 serializePost(_notification.post_id, me); - break; - default: - console.error(`Unknown type: ${_notification.type}`); - break; - } - - resolve(_notification); -}); diff --git a/src/api/serializers/post-reaction.ts b/src/api/serializers/post-reaction.ts deleted file mode 100644 index b8807a741c..0000000000 --- a/src/api/serializers/post-reaction.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import Reaction from '../models/post-reaction'; -import serializeUser from './user'; - -/** - * Serialize a reaction - * - * @param {any} reaction - * @param {any} me? - * @return {Promise<any>} - */ -export default ( - reaction: any, - me?: any -) => new Promise<any>(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 serializeUser(_reaction.user_id, me); - - resolve(_reaction); -}); diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts deleted file mode 100644 index 03fd120772..0000000000 --- a/src/api/serializers/post.ts +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import { default as Post, IPost } from '../models/post'; -import Reaction from '../models/post-reaction'; -import { IUser } from '../models/user'; -import Vote from '../models/poll-vote'; -import serializeApp from './app'; -import serializeChannel from './channel'; -import serializeUser from './user'; -import serializeDriveFile from './drive-file'; -import parse from '../common/text'; -import rap from '@prezzemolo/rap'; - -/** - * Serialize a post - * - * @param post target - * @param me? serializee - * @param options? serialize options - * @return response - */ -const self = 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 = serializeUser(_post.user_id, meId); - - // Populate app - if (_post.app_id) { - _post.app = serializeApp(_post.app_id); - } - - // Populate channel - if (_post.channel_id) { - _post.channel = serializeChannel(_post.channel_id); - } - - // Populate media - if (_post.media_ids) { - _post.media = Promise.all(_post.media_ids.map(fileId => - serializeDriveFile(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 = self(_post.reply_id, meId, { - detail: false - }); - } - - if (_post.repost_id) { - // Populate repost - _post.repost = self(_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; -}; - -export default self; diff --git a/src/api/serializers/signin.ts b/src/api/serializers/signin.ts deleted file mode 100644 index 4068067678..0000000000 --- a/src/api/serializers/signin.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Module dependencies - */ -import deepcopy = require('deepcopy'); - -/** - * Serialize a signin record - * - * @param {any} record - * @return {Promise<any>} - */ -export default ( - record: any -) => new Promise<any>(async (resolve, reject) => { - - const _record = deepcopy(record); - - // Rename _id to id - _record.id = _record._id; - delete _record._id; - - resolve(_record); -}); diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts deleted file mode 100644 index 3d84156606..0000000000 --- a/src/api/serializers/user.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import { default as User, IUser } from '../models/user'; -import serializePost from './post'; -import Following from '../models/following'; -import getFriends from '../common/get-friends'; -import config from '../../conf'; -import rap from '@prezzemolo/rap'; - -/** - * Serialize a user - * - * @param user target - * @param me? serializee - * @param options? serialize options - * @return response - */ -export default ( - user: string | mongo.ObjectID | IUser, - me?: string | mongo.ObjectID | IUser, - options?: { - detail?: boolean, - includeSecrets?: boolean - } -) => new Promise<any>(async (resolve, reject) => { - - const opts = Object.assign({ - detail: false, - includeSecrets: false - }, options); - - let _user: any; - - const fields = opts.detail ? { - settings: false - } : { - settings: false, - client_settings: false, - profile: false, - keywords: false, - 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; - - // Remove private properties - delete _user.password; - delete _user.token; - delete _user.username_lower; - if (_user.twitter) { - delete _user.twitter.access_token; - delete _user.twitter.access_token_secret; - } - delete _user.line; - - // Visible via only the official client - if (!opts.includeSecrets) { - delete _user.email; - delete _user.client_settings; - } - - _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)) { - // If 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; - })(); - - // If 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; - })(); - } - - if (opts.detail) { - if (_user.pinned_post_id) { - // Populate pinned post - _user.pinned_post = serializePost(_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); -}); -/* -function img(url) { - return { - thumbnail: { - large: `${url}`, - medium: '', - small: '' - } - }; -} -*/ diff --git a/src/api/server.ts b/src/api/server.ts index 026357b465..e89d196096 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -26,9 +26,7 @@ app.use(bodyParser.json({ } } })); -app.use(cors({ - origin: true -})); +app.use(cors()); app.get('/', (req, res) => { res.send('YEE HAW'); @@ -49,13 +47,6 @@ endpoints.forEach(endpoint => app.post('/signup', require('./private/signup').default); app.post('/signin', require('./private/signin').default); -app.use((req, res, next) => { - // req.headers['cookie'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - res.locals.user = ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1]; - next(); -}); - require('./service/github')(app); require('./service/twitter')(app); diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts index f164cdc458..adcd5ac49b 100644 --- a/src/api/service/twitter.ts +++ b/src/api/service/twitter.ts @@ -5,27 +5,51 @@ import * as uuid from 'uuid'; // const Twitter = require('twitter'); import autwh from 'autwh'; import redis from '../../db/redis'; -import User from '../models/user'; -import serialize from '../serializers/user'; +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<any> => { - if (res.locals.user == null) return res.send('plz signin'); + 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({ - token: res.locals.user + token: userToken }, { - $set: { - twitter: null - } - }); + $set: { + twitter: null + } + }); res.send(`Twitterの連携を解除しました :v:`); // Publish i updated event - event(user._id, 'i_updated', await serialize(user, user, { + event(user._id, 'i_updated', await pack(user, user, { detail: true, includeSecrets: true })); @@ -50,9 +74,16 @@ module.exports = (app: express.Application) => { }); app.get('/connect/twitter', async (req, res): Promise<any> => { - if (res.locals.user == null) return res.send('plz signin'); + 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(res.locals.user, JSON.stringify(ctx)); + redis.set(userToken, JSON.stringify(ctx)); res.redirect(ctx.url); }); @@ -77,7 +108,9 @@ module.exports = (app: express.Application) => { }); app.get('/tw/cb', (req, res): any => { - if (res.locals.user == null) { + const userToken = getUserToken(req); + + if (userToken == null) { // req.headers['cookie'] は常に string ですが、型定義の都合上 // string | string[] になっているので string を明示しています const cookies = cookie.parse((req.headers['cookie'] as string || '')); @@ -86,6 +119,7 @@ module.exports = (app: express.Application) => { if (sessid == undefined) { res.status(400).send('invalid session'); + return; } redis.get(sessid, async (_, ctx) => { @@ -97,16 +131,24 @@ module.exports = (app: express.Application) => { if (user == null) { res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); + return; } signin(res, user, true); }); } else { - redis.get(res.locals.user, async (_, ctx) => { - const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); + 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({ - token: res.locals.user + token: userToken }, { $set: { twitter: { @@ -121,7 +163,7 @@ module.exports = (app: express.Application) => { res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); // Publish i updated event - event(user._id, 'i_updated', await serialize(user, user, { + event(user._id, 'i_updated', await pack(user, user, { detail: true, includeSecrets: true })); diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts index 7c8f3bfec8..10078337c3 100644 --- a/src/api/stream/home.ts +++ b/src/api/stream/home.ts @@ -3,24 +3,53 @@ import * as redis from 'redis'; import * as debug from 'debug'; import User from '../models/user'; -import serializePost from '../serializers/post'; +import Mute from '../models/mute'; +import { pack as packPost } from '../models/post'; import readNotification from '../common/read-notification'; const log = debug('misskey'); -export default function homeStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { +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': - connection.send(data); + 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 serializePost(postId, user, { + const post = await packPost(postId, user, { detail: true }); connection.send(JSON.stringify({ |