diff --git a/CHANGELOG.md b/CHANGELOG.md index ca41d016c..f8018e4e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,50 @@ ChangeLog (Release Notes) ========================= 主に notable な changes を書いていきます +2807 (2017/11/02) +----------------- +* いい感じに + +2805 (2017/11/02) +----------------- +* いい感じに + +2801 (2017/11/01) +----------------- +* チャンネルのWatch実装 + +2799 (2017/11/01) +----------------- +* いい感じに + +2795 (2017/11/01) +----------------- +* いい感じに + +2793 (2017/11/01) +----------------- +* なんか + +2783 (2017/11/01) +----------------- +* なんか + +2777 (2017/11/01) +----------------- +* 細かいブラッシュアップ + +2775 (2017/11/01) +----------------- +* Fix: バグ修正 + +2769 (2017/11/01) +----------------- +* New: チャンネルシステム + +2752 (2017/10/30) +----------------- +* New: 未読の通知がある場合アイコンを表示するように + 2747 (2017/10/25) ----------------- * Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89) diff --git a/docs/setup.en.md b/docs/setup.en.md index 3e4893534..dbc0599b5 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -25,6 +25,7 @@ Note that Misskey uses following subdomains: * **api**.*{primary domain}* * **auth**.*{primary domain}* * **about**.*{primary domain}* +* **ch**.*{primary domain}* * **stats**.*{primary domain}* * **status**.*{primary domain}* * **dev**.*{primary domain}* diff --git a/docs/setup.ja.md b/docs/setup.ja.md index 4f48a0808..602fd9b6a 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -26,6 +26,7 @@ Misskeyは以下のサブドメインを使います: * **api**.*{primary domain}* * **auth**.*{primary domain}* * **about**.*{primary domain}* +* **ch**.*{primary domain}* * **stats**.*{primary domain}* * **status**.*{primary domain}* * **dev**.*{primary domain}* diff --git a/locales/en.yml b/locales/en.yml index 03d5306d3..52e8dfdb4 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -164,6 +164,19 @@ common: mk-uploader: waiting: "Waiting" +ch: + tags: + mk-index: + new: "Create new channel" + channel-title: "Channel title" + + mk-channel-form: + textarea: "Write here" + upload: "Upload" + drive: "Drive" + post: "Do" + posting: "Doing" + desktop: tags: mk-api-info: @@ -241,6 +254,7 @@ desktop: mk-ui-header-nav: home: "Home" messaging: "Messages" + ch: "Channels" info: "News" mk-ui-header-search: @@ -353,6 +367,9 @@ desktop: mobile: tags: + mk-selectdrive-page: + select-file: "Select file(s)" + mk-drive-file-viewer: download: "Download" rename: "Rename" @@ -389,6 +406,7 @@ mobile: mk-notifications-page: notifications: "Notifications" + read-all: "Are you sure you want to mark all unread notifications as read?" mk-post-page: title: "Post" @@ -490,6 +508,7 @@ mobile: home: "Home" notifications: "Notifications" messaging: "Messages" + ch: "Channels" drive: "Drive" settings: "Settings" about: "About Misskey" diff --git a/locales/ja.yml b/locales/ja.yml index b640f0f24..dcd012bb8 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -84,7 +84,7 @@ common: no-internet: "インターネットに接続されていません" no-internet-desc: "ネットワークには接続されていますが、インターネットには接続されていないようです。お使いのPCのインターネット接続が正常か確認してください。" no-server: "Misskeyのサーバーに接続できません" - no-server-desc: "お使いのPCのネットワーク接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。" + no-server-desc: "お使いのPCのインターネット接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。" success: "Misskeyのサーバーに接続できました" success-desc: "正常に接続できるようです。ページを再度読み込みしてください。" @@ -164,6 +164,19 @@ common: mk-uploader: waiting: "待機中" +ch: + tags: + mk-index: + new: "チャンネルを作成" + channel-title: "チャンネルのタイトル" + + mk-channel-form: + textarea: "書いて" + upload: "アップロード" + drive: "ドライブ" + post: "やる" + posting: "やってます" + desktop: tags: mk-api-info: @@ -241,6 +254,7 @@ desktop: mk-ui-header-nav: home: "ホーム" messaging: "メッセージ" + ch: "チャンネル" info: "お知らせ" mk-ui-header-search: @@ -353,6 +367,9 @@ desktop: mobile: tags: + mk-selectdrive-page: + select-file: "ファイルを選択" + mk-drive-file-viewer: download: "ダウンロード" rename: "名前を変更" @@ -389,6 +406,7 @@ mobile: mk-notifications-page: notifications: "通知" + read-all: "すべての通知を既読にしますか?" mk-post-page: title: "投稿" @@ -490,6 +508,7 @@ mobile: home: "ホーム" notifications: "通知" messaging: "メッセージ" + ch: "チャンネル" search: "検索" drive: "ドライブ" settings: "設定" diff --git a/package.json b/package.json index 8b32db9a8..dd0da40d7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "misskey", "author": "syuilo ", - "version": "0.0.2747", + "version": "0.0.2807", "license": "MIT", "description": "A miniblog-based SNS", "bugs": "https://github.com/syuilo/misskey/issues", @@ -18,22 +18,22 @@ "clean": "gulp clean", "cleanall": "gulp cleanall", "lint": "gulp lint", - "test": "gulp test" + "test": "gulp test" }, "devDependencies": { - "@types/bcryptjs": "2.4.0", + "@types/bcryptjs": "2.4.1", "@types/body-parser": "1.16.7", "@types/chai": "4.0.4", "@types/chai-http": "3.0.3", - "@types/chalk": "0.4.31", + "@types/chalk": "2.2.0", "@types/compression": "0.0.34", "@types/cors": "2.8.1", "@types/debug": "0.0.30", "@types/deep-equal": "1.0.1", - "@types/elasticsearch": "5.0.14", + "@types/elasticsearch": "5.0.17", "@types/event-stream": "3.3.32", "@types/express": "4.0.37", - "@types/gm": "1.17.32", + "@types/gm": "1.17.33", "@types/gulp": "4.0.3", "@types/gulp-htmlmin": "1.3.30", "@types/gulp-mocha": "0.0.30", @@ -41,30 +41,32 @@ "@types/gulp-replace": "0.0.30", "@types/gulp-tslint": "3.6.31", "@types/gulp-typescript": "2.13.0", - "@types/gulp-uglify": "0.0.30", - "@types/gulp-util": "3.0.31", + "@types/gulp-uglify": "3.0.3", + "@types/gulp-util": "3.0.33", "@types/inquirer": "0.0.34", "@types/is-root": "1.0.0", "@types/is-url": "1.2.28", - "@types/js-yaml": "3.9.0", - "@types/mocha": "2.2.43", + "@types/js-yaml": "3.9.1", + "@types/mocha": "2.2.44", "@types/mongodb": "2.2.13", "@types/monk": "1.0.6", - "@types/morgan": "1.7.33", + "@types/morgan": "1.7.35", "@types/ms": "0.7.30", - "@types/multer": "1.3.2", - "@types/node": "8.0.33", + "@types/multer": "1.3.5", + "@types/node": "8.0.47", "@types/ratelimiter": "2.1.28", - "@types/redis": "2.6.0", - "@types/request": "2.0.4", - "@types/rimraf": "2.0.0", - "@types/riot": "3.6.0", - "@types/serve-favicon": "2.2.28", - "@types/uuid": "3.4.2", + "@types/redis": "2.8.1", + "@types/request": "2.0.7", + "@types/rimraf": "2.0.2", + "@types/riot": "3.6.1", + "@types/serve-favicon": "2.2.29", + "@types/uuid": "3.4.3", + "@types/webpack": "3.0.14", + "@types/uuid": "3.4.3", "@types/webpack": "3.0.13", - "@types/webpack-stream": "3.2.7", + "@types/webpack-stream": "3.2.8", "@types/websocket": "0.0.34", - "awesome-typescript-loader": "3.2.3", + "awesome-typescript-loader": "3.3.0", "chai": "4.1.2", "chai-http": "3.0.0", "css-loader": "0.28.7", @@ -78,30 +80,31 @@ "gulp-rename": "1.2.2", "gulp-replace": "0.6.1", "gulp-tslint": "8.1.2", - "gulp-typescript": "3.2.2", + "gulp-typescript": "3.2.3", "gulp-uglify": "3.0.0", "gulp-util": "3.0.8", - "mocha": "3.5.3", + "mocha": "4.0.1", "riot-tag-loader": "1.0.0", "string-replace-webpack-plugin": "0.1.3", "style-loader": "0.19.0", "stylus": "0.54.5", "stylus-loader": "3.0.1", "swagger-jsdoc": "1.9.7", - "tslint": "5.7.0", + "tslint": "5.8.0", "uglify-es": "3.0.27", "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony", - "uglifyjs-webpack-plugin": "1.0.0-beta.2", + "uglifyjs-webpack-plugin": "1.0.1", "webpack": "3.8.1" }, "dependencies": { + "@prezzemolo/rap": "0.1.2", "accesses": "2.5.0", "animejs": "2.2.0", "autwh": "0.0.1", "bcryptjs": "2.4.3", "body-parser": "1.18.2", "cafy": "3.0.0", - "chalk": "2.1.0", + "chalk": "2.3.0", "compression": "1.7.1", "cors": "2.8.4", "cropperjs": "1.1.3", @@ -114,7 +117,7 @@ "elasticsearch": "13.3.1", "escape-regexp": "0.0.1", "express": "4.15.4", - "file-type": "6.2.0", + "file-type": "7.2.0", "fuckadblock": "3.2.1", "gm": "1.23.0", "inquirer": "3.3.0", @@ -140,7 +143,7 @@ "redis": "2.8.0", "request": "2.83.0", "rimraf": "2.6.2", - "riot": "3.7.3", + "riot": "3.7.4", "rndstr": "1.0.0", "s-age": "1.1.0", "serve-favicon": "2.4.5", @@ -149,7 +152,7 @@ "tcp-port-used": "0.1.2", "textarea-caret": "3.0.2", "ts-node": "3.3.0", - "typescript": "2.5.3", + "typescript": "2.6.1", "uuid": "3.1.0", "vhost": "3.0.2", "websocket": "1.0.25", diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts index 714eeb520..f9c22ccac 100644 --- a/src/api/common/add-file-to-drive.ts +++ b/src/api/common/add-file-to-drive.ts @@ -4,14 +4,27 @@ import * as gm from 'gm'; import * as debug from 'debug'; import fileType = require('file-type'); import prominence = require('prominence'); -import DriveFile from '../models/drive-file'; +import DriveFile, { getGridFSBucket } from '../models/drive-file'; import DriveFolder from '../models/drive-folder'; import serialize from '../serializers/drive-file'; import event from '../event'; import config from '../../conf'; +import { Duplex } from 'stream'; const log = debug('misskey:register-drive-file'); +const addToGridFS = (name, binary, metadata): Promise => new Promise(async (resolve, reject) => { + const dataStream = new Duplex(); + dataStream.push(binary); + dataStream.push(null); + + const bucket = await getGridFSBucket(); + const writeStream = bucket.openUploadStream(name, { metadata }); + writeStream.once('finish', (doc) => { resolve(doc); }); + writeStream.on('error', reject); + dataStream.pipe(writeStream); +}); + /** * Add file to drive * @@ -58,7 +71,7 @@ export default ( // Generate hash const hash = crypto - .createHash('sha256') + .createHash('md5') .update(data) .digest('hex') as string; @@ -67,8 +80,8 @@ export default ( if (!force) { // Check if there is a file with the same hash const much = await DriveFile.findOne({ - user_id: user._id, - hash: hash + md5: hash, + 'metadata.user_id': user._id }); if (much !== null) { @@ -82,13 +95,13 @@ export default ( // Calculate drive usage const usage = ((await DriveFile .aggregate([ - { $match: { user_id: user._id } }, + { $match: { 'metadata.user_id': user._id } }, { $project: { - datasize: true + length: true }}, { $group: { _id: null, - usage: { $sum: '$datasize' } + usage: { $sum: '$length' } }} ]))[0] || { usage: 0 @@ -131,21 +144,15 @@ export default ( } // Create DriveFile document - const file = await DriveFile.insert({ - created_at: new Date(), + const file = await addToGridFS(`${user._id}/${name}`, data, { user_id: user._id, folder_id: folder !== null ? folder._id : null, - data: data, - datasize: size, type: mime, name: name, comment: comment, - hash: hash, properties: properties }); - delete file.data; - log(`drive file has been created ${file._id}`); resolve(file); diff --git a/src/api/common/read-notification.ts b/src/api/common/read-notification.ts new file mode 100644 index 000000000..3009cc5d0 --- /dev/null +++ b/src/api/common/read-notification.ts @@ -0,0 +1,52 @@ +import * as mongo from 'mongodb'; +import { default as Notification, INotification } from '../models/notification'; +import publishUserStream from '../event'; + +/** + * Mark as read notification(s) + */ +export default ( + user: string | mongo.ObjectID, + message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[] +) => new Promise(async (resolve, reject) => { + + const userId = mongo.ObjectID.prototype.isPrototypeOf(user) + ? user + : new mongo.ObjectID(user); + + const ids: mongo.ObjectID[] = Array.isArray(message) + ? mongo.ObjectID.prototype.isPrototypeOf(message[0]) + ? (message as mongo.ObjectID[]) + : typeof message[0] === 'string' + ? (message as string[]).map(m => new mongo.ObjectID(m)) + : (message as INotification[]).map(m => m._id) + : mongo.ObjectID.prototype.isPrototypeOf(message) + ? [(message as mongo.ObjectID)] + : typeof message === 'string' + ? [new mongo.ObjectID(message)] + : [(message as INotification)._id]; + + // Update documents + await Notification.update({ + _id: { $in: ids }, + is_read: false + }, { + $set: { + is_read: true + } + }, { + multi: true + }); + + // Calc count of my unread notifications + const count = await Notification + .count({ + notifiee_id: userId, + is_read: false + }); + + if (count == 0) { + // 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行 + publishUserStream(userId, 'read_all_notifications'); + } +}); diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index f05762340..afefce39e 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -195,6 +195,11 @@ const endpoints: Endpoint[] = [ withCredential: true, kind: 'notification-read' }, + { + name: 'notifications/get_unread_count', + withCredential: true, + kind: 'notification-read' + }, { name: 'notifications/delete', withCredential: true, @@ -205,11 +210,6 @@ const endpoints: Endpoint[] = [ withCredential: true, kind: 'notification-write' }, - { - name: 'notifications/mark_as_read', - withCredential: true, - kind: 'notification-write' - }, { name: 'notifications/mark_as_read_all', withCredential: true, @@ -474,8 +474,33 @@ const endpoints: Endpoint[] = [ name: 'messaging/messages/create', withCredential: true, kind: 'messaging-write' - } - + }, + { + name: 'channels/create', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 3, + minInterval: ms('10seconds') + } + }, + { + name: 'channels/show' + }, + { + name: 'channels/posts' + }, + { + name: 'channels/watch', + withCredential: true + }, + { + name: 'channels/unwatch', + withCredential: true + }, + { + name: 'channels' + }, ]; export default endpoints; diff --git a/src/api/endpoints/aggregation/posts.ts b/src/api/endpoints/aggregation/posts.ts index 48ee22512..9d8bccbdb 100644 --- a/src/api/endpoints/aggregation/posts.ts +++ b/src/api/endpoints/aggregation/posts.ts @@ -19,7 +19,7 @@ module.exports = params => new Promise(async (res, rej) => { .aggregate([ { $project: { repost_id: '$repost_id', - reply_to_id: '$reply_to_id', + reply_id: '$reply_id', created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { @@ -34,7 +34,7 @@ module.exports = params => new Promise(async (res, rej) => { then: 'repost', else: { $cond: { - if: { $ne: ['$reply_to_id', null] }, + if: { $ne: ['$reply_id', null] }, then: 'reply', else: 'post' } diff --git a/src/api/endpoints/aggregation/posts/reply.ts b/src/api/endpoints/aggregation/posts/reply.ts index 02a60c896..b114c34e1 100644 --- a/src/api/endpoints/aggregation/posts/reply.ts +++ b/src/api/endpoints/aggregation/posts/reply.ts @@ -26,7 +26,7 @@ module.exports = (params) => new Promise(async (res, rej) => { const datas = await Post .aggregate([ - { $match: { reply_to: post._id } }, + { $match: { reply: post._id } }, { $project: { created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST }}, diff --git a/src/api/endpoints/aggregation/users/activity.ts b/src/api/endpoints/aggregation/users/activity.ts index 5a3e78c44..102a71d7c 100644 --- a/src/api/endpoints/aggregation/users/activity.ts +++ b/src/api/endpoints/aggregation/users/activity.ts @@ -40,7 +40,7 @@ module.exports = (params) => new Promise(async (res, rej) => { { $match: { user_id: user._id } }, { $project: { repost_id: '$repost_id', - reply_to_id: '$reply_to_id', + reply_id: '$reply_id', created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { @@ -55,7 +55,7 @@ module.exports = (params) => new Promise(async (res, rej) => { then: 'repost', else: { $cond: { - if: { $ne: ['$reply_to_id', null] }, + if: { $ne: ['$reply_id', null] }, then: 'reply', else: 'post' } diff --git a/src/api/endpoints/aggregation/users/post.ts b/src/api/endpoints/aggregation/users/post.ts index c964815a0..c6a75eee3 100644 --- a/src/api/endpoints/aggregation/users/post.ts +++ b/src/api/endpoints/aggregation/users/post.ts @@ -34,7 +34,7 @@ module.exports = (params) => new Promise(async (res, rej) => { { $match: { user_id: user._id } }, { $project: { repost_id: '$repost_id', - reply_to_id: '$reply_to_id', + reply_id: '$reply_id', created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { @@ -49,7 +49,7 @@ module.exports = (params) => new Promise(async (res, rej) => { then: 'repost', else: { $cond: { - if: { $ne: ['$reply_to_id', null] }, + if: { $ne: ['$reply_id', null] }, then: 'reply', else: 'post' } diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts new file mode 100644 index 000000000..e10c94389 --- /dev/null +++ b/src/api/endpoints/channels.ts @@ -0,0 +1,59 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../models/channel'; +import serialize from '../serializers/channel'; + +/** + * Get all channels + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'max_id' parameter + const [maxId, maxIdErr] = $(params.max_id).optional.id().$; + if (maxIdErr) return rej('invalid max_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'); + } + + // Construct query + const sort = { + _id: -1 + }; + const query = {} as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (maxId) { + query._id = { + $lt: maxId + }; + } + + // Issue query + const channels = await Channel + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(channels.map(async channel => + await serialize(channel, me)))); +}); diff --git a/src/api/endpoints/channels/create.ts b/src/api/endpoints/channels/create.ts new file mode 100644 index 000000000..a8d7c29dc --- /dev/null +++ b/src/api/endpoints/channels/create.ts @@ -0,0 +1,39 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../../models/channel'; +import Watching from '../../models/channel-watching'; +import serialize from '../../serializers/channel'; + +/** + * Create a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'title' parameter + const [title, titleErr] = $(params.title).string().range(1, 100).$; + if (titleErr) return rej('invalid title param'); + + // Create a channel + const channel = await Channel.insert({ + created_at: new Date(), + user_id: user._id, + title: title, + index: 0, + watching_count: 1 + }); + + // Response + res(await serialize(channel)); + + // Create Watching + await Watching.insert({ + created_at: new Date(), + user_id: user._id, + channel_id: channel._id + }); +}); diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts new file mode 100644 index 000000000..fa91fb93e --- /dev/null +++ b/src/api/endpoints/channels/posts.ts @@ -0,0 +1,79 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import { default as Channel, IChannel } from '../../models/channel'; +import { default as Post, IPost } from '../../models/post'; +import serialize from '../../serializers/post'; + +/** + * Show a posts of a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 1000, limitErr] = $(params.limit).optional.number().range(1, 1000).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'since_id' parameter + const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; + if (sinceIdErr) return rej('invalid since_id param'); + + // Get 'max_id' parameter + const [maxId, maxIdErr] = $(params.max_id).optional.id().$; + if (maxIdErr) return rej('invalid max_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'); + } + + // Get 'channel_id' parameter + const [channelId, channelIdErr] = $(params.channel_id).id().$; + if (channelIdErr) return rej('invalid channel_id param'); + + // Fetch channel + const channel: IChannel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + //#region Construct query + const sort = { + _id: -1 + }; + + const query = { + channel_id: channel._id + } as any; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (maxId) { + query._id = { + $lt: maxId + }; + } + //#endregion Construct query + + // Issue query + const posts = await Post + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(posts.map(async (post) => + await serialize(post, user) + ))); +}); diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts new file mode 100644 index 000000000..8861e5459 --- /dev/null +++ b/src/api/endpoints/channels/show.ts @@ -0,0 +1,31 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import { default as Channel, IChannel } from '../../models/channel'; +import serialize from '../../serializers/channel'; + +/** + * Show a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'channel_id' parameter + const [channelId, channelIdErr] = $(params.channel_id).id().$; + if (channelIdErr) return rej('invalid channel_id param'); + + // Fetch channel + const channel: IChannel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + // Serialize + res(await serialize(channel, user)); +}); diff --git a/src/api/endpoints/channels/unwatch.ts b/src/api/endpoints/channels/unwatch.ts new file mode 100644 index 000000000..19d3be118 --- /dev/null +++ b/src/api/endpoints/channels/unwatch.ts @@ -0,0 +1,60 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../../models/channel'; +import Watching from '../../models/channel-watching'; + +/** + * Unwatch a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'channel_id' parameter + const [channelId, channelIdErr] = $(params.channel_id).id().$; + if (channelIdErr) return rej('invalid channel_id param'); + + //#region Fetch channel + const channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + //#endregion + + //#region Check whether not watching + const exist = await Watching.findOne({ + user_id: user._id, + channel_id: channel._id, + deleted_at: { $exists: false } + }); + + if (exist === null) { + return rej('already not watching'); + } + //#endregion + + // Delete watching + await Watching.update({ + _id: exist._id + }, { + $set: { + deleted_at: new Date() + } + }); + + // Send response + res(); + + // Decrement watching count + Channel.update(channel._id, { + $inc: { + watching_count: -1 + } + }); +}); diff --git a/src/api/endpoints/channels/watch.ts b/src/api/endpoints/channels/watch.ts new file mode 100644 index 000000000..030e0dd41 --- /dev/null +++ b/src/api/endpoints/channels/watch.ts @@ -0,0 +1,58 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../../models/channel'; +import Watching from '../../models/channel-watching'; + +/** + * Watch a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'channel_id' parameter + const [channelId, channelIdErr] = $(params.channel_id).id().$; + if (channelIdErr) return rej('invalid channel_id param'); + + //#region Fetch channel + const channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + //#endregion + + //#region Check whether already watching + const exist = await Watching.findOne({ + user_id: user._id, + channel_id: channel._id, + deleted_at: { $exists: false } + }); + + if (exist !== null) { + return rej('already watching'); + } + //#endregion + + // Create Watching + await Watching.insert({ + created_at: new Date(), + user_id: user._id, + channel_id: channel._id + }); + + // Send response + res(); + + // Increment watching count + Channel.update(channel._id, { + $inc: { + watching_count: 1 + } + }); +}); diff --git a/src/api/endpoints/drive.ts b/src/api/endpoints/drive.ts index 41ad6301d..d92473633 100644 --- a/src/api/endpoints/drive.ts +++ b/src/api/endpoints/drive.ts @@ -14,16 +14,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Calculate drive usage const usage = ((await DriveFile .aggregate([ - { $match: { user_id: user._id } }, + { $match: { 'metadata.user_id': user._id } }, { $project: { - datasize: true + length: true } }, { $group: { _id: null, - usage: { $sum: '$datasize' } + usage: { $sum: '$length' } } } ]))[0] || { diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts index a68ae3481..53b48a8be 100644 --- a/src/api/endpoints/drive/files.ts +++ b/src/api/endpoints/drive/files.ts @@ -13,35 +13,35 @@ import serialize from '../../serializers/drive-file'; * @param {any} app * @return {Promise} */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { +module.exports = async (params, user, app) => { // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); + if (limitErr) throw 'invalid limit param'; // Get 'since_id' parameter const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + if (sinceIdErr) throw 'invalid since_id param'; // Get 'max_id' parameter const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); + if (maxIdErr) throw 'invalid max_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'); + throw 'cannot set since_id and max_id'; } // Get 'folder_id' parameter const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); + if (folderIdErr) throw 'invalid folder_id param'; // Construct query const sort = { _id: -1 }; const query = { - user_id: user._id, - folder_id: folderId + 'metadata.user_id': user._id, + 'metadata.folder_id': folderId } as any; if (sinceId) { sort._id = 1; @@ -57,14 +57,11 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { // Issue query const files = await DriveFile .find(query, { - fields: { - data: false - }, limit: limit, sort: sort }); // Serialize - res(await Promise.all(files.map(async file => - await serialize(file)))); -}); + const _files = await Promise.all(files.map(file => serialize(file))); + return _files; +}; diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts index cd0b33f2c..1c818131d 100644 --- a/src/api/endpoints/drive/files/find.ts +++ b/src/api/endpoints/drive/files/find.ts @@ -24,13 +24,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Issue query const files = await DriveFile .find({ - name: name, - user_id: user._id, - folder_id: folderId - }, { - fields: { - data: false - } + 'metadata.name': name, + 'metadata.user_id': user._id, + 'metadata.folder_id': folderId }); // Serialize diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts index 8dbc297e4..3c7cf774f 100644 --- a/src/api/endpoints/drive/files/show.ts +++ b/src/api/endpoints/drive/files/show.ts @@ -12,28 +12,26 @@ import serialize from '../../../serializers/drive-file'; * @param {any} user * @return {Promise} */ -module.exports = (params, user) => new Promise(async (res, rej) => { +module.exports = async (params, user) => { // Get 'file_id' parameter const [fileId, fileIdErr] = $(params.file_id).id().$; - if (fileIdErr) return rej('invalid file_id param'); + if (fileIdErr) throw 'invalid file_id param'; // Fetch file const file = await DriveFile .findOne({ _id: fileId, - user_id: user._id - }, { - fields: { - data: false - } + 'metadata.user_id': user._id }); if (file === null) { - return rej('file-not-found'); + throw 'file-not-found'; } // Serialize - res(await serialize(file, { + const _file = await serialize(file, { detail: true - })); -}); + }); + + return _file; +}; diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts index 1cfbdd8f0..d7b858c2b 100644 --- a/src/api/endpoints/drive/files/update.ts +++ b/src/api/endpoints/drive/files/update.ts @@ -24,11 +24,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const file = await DriveFile .findOne({ _id: fileId, - user_id: user._id - }, { - fields: { - data: false - } + 'metadata.user_id': user._id }); if (file === null) { @@ -38,7 +34,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'name' parameter const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$; if (nameErr) return rej('invalid name param'); - if (name) file.name = name; + if (name) file.metadata.name = name; // Get 'folder_id' parameter const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$; @@ -46,7 +42,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (folderId !== undefined) { if (folderId === null) { - file.folder_id = null; + file.metadata.folder_id = null; } else { // Fetch folder const folder = await DriveFolder @@ -59,14 +55,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { return rej('folder-not-found'); } - file.folder_id = folder._id; + file.metadata.folder_id = folder._id; } } - DriveFile.update(file._id, { + await DriveFile.update(file._id, { $set: { - name: file.name, - folder_id: file.folder_id + 'metadata.name': file.metadata.name, + 'metadata.folder_id': file.metadata.folder_id } }); diff --git a/src/api/endpoints/drive/folders/find.ts b/src/api/endpoints/drive/folders/find.ts index cdf055839..a5eb8e015 100644 --- a/src/api/endpoints/drive/folders/find.ts +++ b/src/api/endpoints/drive/folders/find.ts @@ -30,6 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Serialize - res(await Promise.all(folders.map(async folder => - await serialize(folder)))); + res(await Promise.all(folders.map(folder => serialize(folder)))); }); diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts index eec275787..4f2e3d2a7 100644 --- a/src/api/endpoints/drive/folders/update.ts +++ b/src/api/endpoints/drive/folders/update.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import DriveFolder from '../../../models/drive-folder'; import { isValidFolderName } from '../../../models/drive-folder'; -import serialize from '../../../serializers/drive-file'; +import serialize from '../../../serializers/drive-folder'; import event from '../../../event'; /** diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts index 5575fb741..607e0768a 100644 --- a/src/api/endpoints/i/notifications.ts +++ b/src/api/endpoints/i/notifications.ts @@ -5,6 +5,7 @@ import $ from 'cafy'; import Notification from '../../models/notification'; import serialize from '../../serializers/notification'; import getFriends from '../../common/get-friends'; +import read from '../../common/read-notification'; /** * Get notifications @@ -91,17 +92,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Mark as read all if (notifications.length > 0 && markAsRead) { - const ids = notifications - .filter(x => x.is_read == false) - .map(x => x._id); - - // Update documents - await Notification.update({ - _id: { $in: ids } - }, { - $set: { is_read: true } - }, { - multi: true - }); + read(user._id, notifications); } }); diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts index 8af55d850..149852c09 100644 --- a/src/api/endpoints/messaging/messages/create.ts +++ b/src/api/endpoints/messaging/messages/create.ts @@ -54,9 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (fileId !== undefined) { file = await DriveFile.findOne({ _id: fileId, - user_id: user._id - }, { - data: false + 'metadata.user_id': user._id }); if (file === null) { diff --git a/src/api/endpoints/notifications/get_unread_count.ts b/src/api/endpoints/notifications/get_unread_count.ts new file mode 100644 index 000000000..9514e7871 --- /dev/null +++ b/src/api/endpoints/notifications/get_unread_count.ts @@ -0,0 +1,23 @@ +/** + * Module dependencies + */ +import Notification from '../../models/notification'; + +/** + * Get count of unread notifications + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const count = await Notification + .count({ + notifiee_id: user._id, + is_read: false + }); + + res({ + count: count + }); +}); diff --git a/src/api/endpoints/notifications/mark_as_read.ts b/src/api/endpoints/notifications/mark_as_read.ts deleted file mode 100644 index 5cce33e85..000000000 --- a/src/api/endpoints/notifications/mark_as_read.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Notification from '../../models/notification'; -import serialize from '../../serializers/notification'; -import event from '../../event'; - -/** - * Mark as read a notification - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - const [notificationId, notificationIdErr] = $(params.notification_id).id().$; - if (notificationIdErr) return rej('invalid notification_id param'); - - // Get notification - const notification = await Notification - .findOne({ - _id: notificationId, - i: user._id - }); - - if (notification === null) { - return rej('notification-not-found'); - } - - // Update - notification.is_read = true; - Notification.update({ _id: notification._id }, { - $set: { - is_read: true - } - }); - - // Response - res(); - - // Serialize - const notificationObj = await serialize(notification); - - // Publish read_notification event - event(user._id, 'read_notification', notificationObj); -}); diff --git a/src/api/endpoints/notifications/mark_as_read_all.ts b/src/api/endpoints/notifications/mark_as_read_all.ts new file mode 100644 index 000000000..3550e344c --- /dev/null +++ b/src/api/endpoints/notifications/mark_as_read_all.ts @@ -0,0 +1,32 @@ +/** + * Module dependencies + */ +import Notification from '../../models/notification'; +import event from '../../event'; + +/** + * Mark as read all notifications + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Update documents + await Notification.update({ + notifiee_id: user._id, + is_read: false + }, { + $set: { + is_read: true + } + }, { + multi: true + }); + + // Response + res(); + + // 全ての通知を読みましたよというイベントを発行 + event(user._id, 'read_all_notifications'); +}); diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts index 23b9bd0b6..f6efcc108 100644 --- a/src/api/endpoints/posts.ts +++ b/src/api/endpoints/posts.ts @@ -62,7 +62,7 @@ module.exports = (params) => new Promise(async (res, rej) => { } if (reply != undefined) { - query.reply_to_id = reply ? { $exists: true, $ne: null } : null; + query.reply_id = reply ? { $exists: true, $ne: null } : null; } if (repost != undefined) { diff --git a/src/api/endpoints/posts/context.ts b/src/api/endpoints/posts/context.ts index cd5f15f48..bad59a6be 100644 --- a/src/api/endpoints/posts/context.ts +++ b/src/api/endpoints/posts/context.ts @@ -49,13 +49,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => { return; } - if (p.reply_to_id) { - await get(p.reply_to_id); + if (p.reply_id) { + await get(p.reply_id); } } - if (post.reply_to_id) { - await get(post.reply_to_id); + if (post.reply_id) { + await get(post.reply_id); } // Serialize diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts index 805dba7f8..4f4b7e2e8 100644 --- a/src/api/endpoints/posts/create.ts +++ b/src/api/endpoints/posts/create.ts @@ -4,16 +4,17 @@ import $ from 'cafy'; import deepEqual = require('deep-equal'); import parse from '../../common/text'; -import Post from '../../models/post'; -import { isValidText } from '../../models/post'; +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 DriveFile from '../../models/drive-file'; import Watching from '../../models/post-watching'; +import ChannelWatching from '../../models/channel-watching'; import serialize from '../../serializers/post'; import notify from '../../common/notify'; import watch from '../../common/watch-post'; -import event from '../../event'; +import { default as event, publishChannelStream } from '../../event'; import config from '../../../conf'; /** @@ -43,9 +44,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // SELECT _id const entity = await DriveFile.findOne({ _id: mediaId, - user_id: user._id - }, { - _id: true + 'metadata.user_id': user._id }); if (entity === null) { @@ -62,7 +61,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { const [repostId, repostIdErr] = $(params.repost_id).optional.id().$; if (repostIdErr) return rej('invalid repost_id'); - let repost = null; + let repost: IPost = null; + let isQuote = false; if (repostId !== undefined) { // Fetch repost to post repost = await Post.findOne({ @@ -84,43 +84,86 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { } }); + isQuote = text != null || files != null; + // 直近と同じRepost対象かつ引用じゃなかったらエラー if (latestPost && latestPost.repost_id && latestPost.repost_id.equals(repost._id) && - text === undefined && files === null) { + !isQuote) { return rej('cannot repost same post that already reposted in your latest post'); } // 直近がRepost対象かつ引用じゃなかったらエラー if (latestPost && latestPost._id.equals(repost._id) && - text === undefined && files === null) { + !isQuote) { return rej('cannot repost your latest post'); } } - // Get 'in_reply_to_post_id' parameter - const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$; - if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id'); + // Get 'reply_id' parameter + const [replyId, replyIdErr] = $(params.reply_id).optional.id().$; + if (replyIdErr) return rej('invalid reply_id'); - let inReplyToPost = null; - if (inReplyToPostId !== undefined) { + let reply: IPost = null; + if (replyId !== undefined) { // Fetch reply - inReplyToPost = await Post.findOne({ - _id: inReplyToPostId + reply = await Post.findOne({ + _id: replyId }); - if (inReplyToPost === null) { + if (reply === null) { return rej('in reply to post is not found'); } // 返信対象が引用でないRepostだったらエラー - if (inReplyToPost.repost_id && !inReplyToPost.text && !inReplyToPost.media_ids) { + if (reply.repost_id && !reply.text && !reply.media_ids) { return rej('cannot reply to repost'); } } + // Get 'channel_id' parameter + const [channelId, channelIdErr] = $(params.channel_id).optional.id().$; + if (channelIdErr) return rej('invalid channel_id'); + + let channel: IChannel = null; + if (channelId !== undefined) { + // Fetch channel + channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + // 返信対象の投稿がこのチャンネルじゃなかったらダメ + if (reply && !channelId.equals(reply.channel_id)) { + return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません'); + } + + // Repost対象の投稿がこのチャンネルじゃなかったらダメ + if (repost && !channelId.equals(repost.channel_id)) { + return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません'); + } + + // 引用ではないRepostはダメ + if (repost && !isQuote) { + return rej('チャンネル内部では引用ではないRepostをすることはできません'); + } + } else { + // 返信対象の投稿がチャンネルへの投稿だったらダメ + if (reply && reply.channel_id != null) { + return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません'); + } + + // Repost対象の投稿がチャンネルへの投稿だったらダメ + if (repost && repost.channel_id != null) { + return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません'); + } + } + // Get 'poll' parameter const [poll, pollErr] = $(params.poll).optional.strict.object() .have('choices', $().array('string') @@ -148,15 +191,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { if (user.latest_post) { if (deepEqual({ text: user.latest_post.text, - reply: user.latest_post.reply_to_id ? user.latest_post.reply_to_id.toString() : null, + reply: user.latest_post.reply_id ? user.latest_post.reply_id.toString() : null, repost: user.latest_post.repost_id ? user.latest_post.repost_id.toString() : null, media_ids: (user.latest_post.media_ids || []).map(id => id.toString()) }, { - text: text, - reply: inReplyToPost ? inReplyToPost._id.toString() : null, - repost: repost ? repost._id.toString() : null, - media_ids: (files || []).map(file => file._id.toString()) - })) { + text: text, + reply: reply ? reply._id.toString() : null, + repost: repost ? repost._id.toString() : null, + media_ids: (files || []).map(file => file._id.toString()) + })) { return rej('duplicate'); } } @@ -164,8 +207,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // 投稿を作成 const post = await Post.insert({ created_at: new Date(), + channel_id: channel ? channel._id : undefined, + index: channel ? channel.index + 1 : undefined, media_ids: files ? files.map(file => file._id) : undefined, - reply_to_id: inReplyToPost ? inReplyToPost._id : undefined, + reply_id: reply ? reply._id : undefined, repost_id: repost ? repost._id : undefined, poll: poll, text: text, @@ -179,8 +224,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // Reponse res(postObj); - // ----------------------------------------------------------- - // Post processes + //#region Post processes User.update({ _id: user._id }, { $set: { @@ -203,23 +247,51 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { } } - // Publish event to myself's stream - event(user._id, 'post', postObj); + // タイムラインへの投稿 + if (!channel) { + // Publish event to myself's stream + event(user._id, 'post', postObj); - // Fetch all followers - const followers = await Following - .find({ - followee_id: user._id, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - follower_id: true, - _id: false + // Fetch all followers + const followers = await Following + .find({ + followee_id: user._id, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }, { + follower_id: true, + _id: false + }); + + // Publish event to followers stream + followers.forEach(following => + event(following.follower_id, 'post', postObj)); + } + + // チャンネルへの投稿 + if (channel) { + // Increment channel index(posts count) + Channel.update({ _id: channel._id }, { + $inc: { + index: 1 + } }); - // Publish event to followers stream - followers.forEach(following => - event(following.follower_id, 'post', postObj)); + // Publish event to channel + publishChannelStream(channel._id, 'post', postObj); + + // Get channel watchers + const watches = await ChannelWatching.find({ + channel_id: channel._id, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }); + + // チャンネルの視聴者(のタイムライン)に配信 + watches.forEach(w => { + event(w.user_id, 'post', postObj); + }); + } // Increment my posts count User.update({ _id: user._id }, { @@ -229,23 +301,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { }); // If has in reply to post - if (inReplyToPost) { + if (reply) { // Increment replies count - Post.update({ _id: inReplyToPost._id }, { + Post.update({ _id: reply._id }, { $inc: { replies_count: 1 } }); // 自分自身へのリプライでない限りは通知を作成 - notify(inReplyToPost.user_id, user._id, 'reply', { + notify(reply.user_id, user._id, 'reply', { post_id: post._id }); // Fetch watchers Watching .find({ - post_id: inReplyToPost._id, + post_id: reply._id, user_id: { $ne: user._id }, // 削除されたドキュメントは除く deleted_at: { $exists: false } @@ -265,10 +337,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // この投稿をWatchする // TODO: ユーザーが「返信したときに自動でWatchする」設定を // オフにしていた場合はしない - watch(user._id, inReplyToPost); + watch(user._id, reply); // Add mention - addMention(inReplyToPost.user_id, 'reply'); + addMention(reply.user_id, 'reply'); } // If it is repost @@ -369,7 +441,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { if (mentionee == null) return; // 既に言及されたユーザーに対する返信や引用repostの場合も無視 - if (inReplyToPost && inReplyToPost.user_id.equals(mentionee._id)) return; + if (reply && reply.user_id.equals(mentionee._id)) return; if (repost && repost.user_id.equals(mentionee._id)) return; // Add mention @@ -406,4 +478,6 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { } }); } + + //#endregion }); diff --git a/src/api/endpoints/posts/replies.ts b/src/api/endpoints/posts/replies.ts index 89f4d9984..3fd6a4676 100644 --- a/src/api/endpoints/posts/replies.ts +++ b/src/api/endpoints/posts/replies.ts @@ -40,7 +40,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Issue query const replies = await Post - .find({ reply_to_id: post._id }, { + .find({ reply_id: post._id }, { limit: limit, skip: offset, sort: { diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts index 314e99234..203413e23 100644 --- a/src/api/endpoints/posts/timeline.ts +++ b/src/api/endpoints/posts/timeline.ts @@ -2,7 +2,9 @@ * Module dependencies */ import $ from 'cafy'; +import rap from '@prezzemolo/rap'; import Post from '../../models/post'; +import ChannelWatching from '../../models/channel-watching'; import getFriends from '../../common/get-friends'; import serialize from '../../serializers/post'; @@ -14,36 +16,62 @@ import serialize from '../../serializers/post'; * @param {any} app * @return {Promise} */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { +module.exports = async (params, user, app) => { // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); + if (limitErr) throw 'invalid limit param'; // Get 'since_id' parameter const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + if (sinceIdErr) throw 'invalid since_id param'; // Get 'max_id' parameter const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); + if (maxIdErr) throw 'invalid max_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'); + throw 'cannot set since_id and max_id'; } - // ID list of the user $self and other users who the user follows - const followingIds = await getFriends(user._id); + const { followingIds, watchChannelIds } = await rap({ + // ID list of the user itself and other users who the user follows + followingIds: getFriends(user._id), + // Watchしているチャンネルを取得 + watchChannelIds: ChannelWatching.find({ + user_id: user._id, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }).then(watches => watches.map(w => w.channel_id)) + }); - // Construct query + //#region Construct query const sort = { _id: -1 }; + const query = { - user_id: { - $in: followingIds - } + $or: [{ + // フォローしている人のタイムラインへの投稿 + user_id: { + $in: followingIds + }, + // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る + $or: [{ + channel_id: { + $exists: false + } + }, { + channel_id: null + }] + }, { + // Watchしているチャンネルへの投稿 + channel_id: { + $in: watchChannelIds + } + }] } as any; + if (sinceId) { sort._id = 1; query._id = { @@ -54,6 +82,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { $lt: maxId }; } + //#endregion // Issue query const timeline = await Post @@ -63,7 +92,6 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { }); // Serialize - res(await Promise.all(timeline.map(async post => - await serialize(post, user) - ))); -}); + const _timeline = await Promise.all(timeline.map(post => serialize(post, user))); + return _timeline; +}; diff --git a/src/api/endpoints/posts/trend.ts b/src/api/endpoints/posts/trend.ts index 3277206d2..64a195dff 100644 --- a/src/api/endpoints/posts/trend.ts +++ b/src/api/endpoints/posts/trend.ts @@ -48,7 +48,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } as any; if (reply != undefined) { - query.reply_to_id = reply ? { $exists: true, $ne: null } : null; + query.reply_id = reply ? { $exists: true, $ne: null } : null; } if (repost != undefined) { diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts index 2e0e2e40a..bb0f3b4ce 100644 --- a/src/api/endpoints/users/get_frequently_replied_users.ts +++ b/src/api/endpoints/users/get_frequently_replied_users.ts @@ -27,7 +27,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Fetch recent posts const recentPosts = await Post.find({ user_id: user._id, - reply_to_id: { + reply_id: { $exists: true, $ne: null } @@ -38,7 +38,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { limit: 1000, fields: { _id: false, - reply_to_id: true + reply_id: true } }); @@ -49,7 +49,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const replyTargetPosts = await Post.find({ _id: { - $in: recentPosts.map(p => p.reply_to_id) + $in: recentPosts.map(p => p.reply_id) }, user_id: { $ne: user._id diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts index e37b66077..d8204b8b8 100644 --- a/src/api/endpoints/users/posts.ts +++ b/src/api/endpoints/users/posts.ts @@ -85,7 +85,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { } if (!includeReplies) { - query.reply_to_id = null; + query.reply_id = null; } if (withMedia) { diff --git a/src/api/event.ts b/src/api/event.ts index 9613a9f7c..909b0d255 100644 --- a/src/api/event.ts +++ b/src/api/event.ts @@ -25,6 +25,10 @@ class MisskeyEvent { this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); } + public publishChannelStream(channelId: ID, type: string, value?: any): void { + this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value); + } + private publish(channel: string, type: string, value?: any): void { const message = value == null ? { type: type } : @@ -41,3 +45,5 @@ export default ev.publishUserStream.bind(ev); export const publishPostStream = ev.publishPostStream.bind(ev); export const publishMessagingStream = ev.publishMessagingStream.bind(ev); + +export const publishChannelStream = ev.publishChannelStream.bind(ev); diff --git a/src/api/models/channel-watching.ts b/src/api/models/channel-watching.ts new file mode 100644 index 000000000..6184ae408 --- /dev/null +++ b/src/api/models/channel-watching.ts @@ -0,0 +1,3 @@ +import db from '../../db/mongodb'; + +export default db.get('channel_watching') as any; // fuck type definition diff --git a/src/api/models/channel.ts b/src/api/models/channel.ts new file mode 100644 index 000000000..c80e84dbc --- /dev/null +++ b/src/api/models/channel.ts @@ -0,0 +1,14 @@ +import * as mongo from 'mongodb'; +import db from '../../db/mongodb'; + +const collection = db.get('channels'); + +export default collection as any; // fuck type definition + +export type IChannel = { + _id: mongo.ObjectID; + created_at: Date; + title: string; + user_id: mongo.ObjectID; + index: number; +}; diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts index 8d158cf56..8968d065c 100644 --- a/src/api/models/drive-file.ts +++ b/src/api/models/drive-file.ts @@ -1,11 +1,22 @@ -import db from '../../db/mongodb'; +import * as mongodb from 'mongodb'; +import monkDb, { nativeDbConn } from '../../db/mongodb'; -const collection = db.get('drive_files'); +const collection = monkDb.get('drive_files.files'); (collection as any).createIndex('hash'); // fuck type definition export default collection as any; // fuck type definition +const getGridFSBucket = async (): Promise => { + const db = await nativeDbConn(); + const bucket = new mongodb.GridFSBucket(db, { + bucketName: 'drive_files' + }); + return bucket; +}; + +export { getGridFSBucket }; + export function validateFileName(name: string): boolean { return ( (name.trim().length > 0) && diff --git a/src/api/models/notification.ts b/src/api/models/notification.ts index 1c1f429a0..1065e8baa 100644 --- a/src/api/models/notification.ts +++ b/src/api/models/notification.ts @@ -1,3 +1,8 @@ +import * as mongo from 'mongodb'; import db from '../../db/mongodb'; export default db.get('notifications') as any; // fuck type definition + +export interface INotification { + _id: mongo.ObjectID; +} diff --git a/src/api/models/post.ts b/src/api/models/post.ts index 8b9f7f5ef..7584ce182 100644 --- a/src/api/models/post.ts +++ b/src/api/models/post.ts @@ -10,9 +10,10 @@ export function isValidText(text: string): boolean { export type IPost = { _id: mongo.ObjectID; + channel_id: mongo.ObjectID; created_at: Date; media_ids: mongo.ObjectID[]; - reply_to_id: mongo.ObjectID; + reply_id: mongo.ObjectID; repost_id: mongo.ObjectID; poll: {}; // todo text: string; diff --git a/src/api/serializers/channel.ts b/src/api/serializers/channel.ts new file mode 100644 index 000000000..3cba39aa1 --- /dev/null +++ b/src/api/serializers/channel.ts @@ -0,0 +1,66 @@ +/** + * 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(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 index b4e2ab064..2af7db572 100644 --- a/src/api/serializers/drive-file.ts +++ b/src/api/serializers/drive-file.ts @@ -31,44 +31,40 @@ export default ( if (mongo.ObjectID.prototype.isPrototypeOf(file)) { _file = await DriveFile.findOne({ _id: file - }, { - fields: { - data: false - } - }); + }); } else if (typeof file === 'string') { _file = await DriveFile.findOne({ _id: new mongo.ObjectID(file) - }, { - fields: { - data: false - } - }); + }); } else { _file = deepcopy(file); } - // Rename _id to id - _file.id = _file._id; - delete _file._id; + if (!_file) return reject('invalid file arg.'); - delete _file.data; + // rendered target + let _target: any = {}; - _file.url = `${config.drive_url}/${_file.id}/${encodeURIComponent(_file.name)}`; + _target.id = _file._id; + _target.created_at = _file.uploadDate; - if (opts.detail && _file.folder_id) { + _target = Object.assign(_target, _file.metadata); + + _target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`; + + if (opts.detail && _target.folder_id) { // Populate folder - _file.folder = await serializeDriveFolder(_file.folder_id, { + _target.folder = await serializeDriveFolder(_target.folder_id, { detail: true }); } - if (opts.detail && _file.tags) { + if (opts.detail && _target.tags) { // Populate tags - _file.tags = await _file.tags.map(async (tag: any) => + _target.tags = await _target.tags.map(async (tag: any) => await serializeDriveTag(tag) ); } - resolve(_file); + resolve(_target); }); diff --git a/src/api/serializers/drive-folder.ts b/src/api/serializers/drive-folder.ts index a42846410..6ebf454a2 100644 --- a/src/api/serializers/drive-folder.ts +++ b/src/api/serializers/drive-folder.ts @@ -44,7 +44,7 @@ const self = ( }); const childFilesCount = await DriveFile.count({ - folder_id: _folder.id + 'metadata.folder_id': _folder.id }); _folder.folders_count = childFoldersCount; diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts index df917a859..03fd12077 100644 --- a/src/api/serializers/post.ts +++ b/src/api/serializers/post.ts @@ -8,9 +8,11 @@ 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 @@ -20,13 +22,13 @@ import parse from '../common/text'; * @param options? serialize options * @return response */ -const self = ( +const self = async ( post: string | mongo.ObjectID | IPost, me?: string | mongo.ObjectID | IUser, options?: { detail: boolean } -) => new Promise(async (resolve, reject) => { +) => { const opts = options || { detail: true, }; @@ -55,6 +57,8 @@ const self = ( _post = deepcopy(post); } + if (!_post) throw 'invalid post arg.'; + const id = _post._id; // Rename _id to id @@ -69,100 +73,120 @@ const self = ( } // Populate user - _post.user = await serializeUser(_post.user_id, meId); + _post.user = serializeUser(_post.user_id, meId); // Populate app if (_post.app_id) { - _post.app = await serializeApp(_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) { - // Populate media - _post.media = await Promise.all(_post.media_ids.map(async fileId => - await serializeDriveFile(fileId) + _post.media = Promise.all(_post.media_ids.map(fileId => + serializeDriveFile(fileId) )); } // When requested a detailed post data if (opts.detail) { // Get previous post info - const prev = await Post.findOne({ - user_id: _post.user_id, - _id: { - $lt: id - } - }, { - fields: { - _id: true - }, - sort: { - _id: -1 - } - }); - _post.prev = prev ? prev._id : null; + _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 - const next = await Post.findOne({ - user_id: _post.user_id, - _id: { - $gt: id - } - }, { - fields: { - _id: true - }, - sort: { - _id: 1 - } - }); - _post.next = next ? next._id : null; + _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_to_id) { + if (_post.reply_id) { // Populate reply to post - _post.reply_to = await self(_post.reply_to_id, meId, { + _post.reply = self(_post.reply_id, meId, { detail: false }); } if (_post.repost_id) { // Populate repost - _post.repost = await self(_post.repost_id, meId, { + _post.repost = self(_post.repost_id, meId, { detail: _post.text == null }); } // Poll if (meId && _post.poll) { - const vote = await Vote - .findOne({ - user_id: meId, - post_id: id - }); + _post.poll = (async (poll) => { + const vote = await Vote + .findOne({ + user_id: meId, + post_id: id + }); - if (vote != null) { - const myChoice = _post.poll.choices - .filter(c => c.id == vote.choice)[0]; + if (vote != null) { + const myChoice = poll.choices + .filter(c => c.id == vote.choice)[0]; - myChoice.is_voted = true; - } + myChoice.is_voted = true; + } + + return poll; + })(_post.poll); } // Fetch my reaction if (meId) { - const reaction = await Reaction - .findOne({ - user_id: meId, - post_id: id, - deleted_at: { $exists: false } - }); + _post.my_reaction = (async () => { + const reaction = await Reaction + .findOne({ + user_id: meId, + post_id: id, + deleted_at: { $exists: false } + }); - if (reaction) { - _post.my_reaction = reaction.reaction; - } + if (reaction) { + return reaction.reaction; + } + + return null; + })(); } } - resolve(_post); -}); + // resolve promises in _post object + _post = await rap(_post); + + return _post; +}; export default self; diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts index 3deff2d00..0d24d6cc0 100644 --- a/src/api/serializers/user.ts +++ b/src/api/serializers/user.ts @@ -8,6 +8,7 @@ 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 @@ -55,6 +56,8 @@ export default ( _user = deepcopy(user); } + if (!_user) return reject('invalid user arg.'); + // Me const meId: mongo.ObjectID = me ? mongo.ObjectID.prototype.isPrototypeOf(me) @@ -104,26 +107,30 @@ export default ( if (meId && !meId.equals(_user.id)) { // If the user is following - const follow = await Following.findOne({ - follower_id: meId, - followee_id: _user.id, - deleted_at: { $exists: false } - }); - _user.is_following = follow !== null; + _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 - const follow2 = await Following.findOne({ - follower_id: _user.id, - followee_id: meId, - deleted_at: { $exists: false } - }); - _user.is_followed = follow2 !== null; + _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 = await serializePost(_user.pinned_post_id, meId, { + _user.pinned_post = serializePost(_user.pinned_post_id, meId, { detail: true }); } @@ -132,23 +139,24 @@ export default ( const myFollowingIds = await getFriends(meId); // Get following you know count - const followingYouKnowCount = await Following.count({ + _user.following_you_know_count = Following.count({ followee_id: { $in: myFollowingIds }, follower_id: _user.id, deleted_at: { $exists: false } }); - _user.following_you_know_count = followingYouKnowCount; // Get followers you know count - const followersYouKnowCount = await Following.count({ + _user.followers_you_know_count = Following.count({ followee_id: _user.id, follower_id: { $in: myFollowingIds }, deleted_at: { $exists: false } }); - _user.followers_you_know_count = followersYouKnowCount; } } + // resolve promises in _user object + _user = await rap(_user); + resolve(_user); }); /* diff --git a/src/api/stream/channel.ts b/src/api/stream/channel.ts new file mode 100644 index 000000000..d67d77cbf --- /dev/null +++ b/src/api/stream/channel.ts @@ -0,0 +1,12 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void { + const channel = request.resourceURL.query.channel; + + // Subscribe channel stream + subscriber.subscribe(`misskey:channel-stream:${channel}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); +} diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts index d5fe01c26..7c8f3bfec 100644 --- a/src/api/stream/home.ts +++ b/src/api/stream/home.ts @@ -4,6 +4,7 @@ import * as debug from 'debug'; import User from '../models/user'; import serializePost from '../serializers/post'; +import readNotification from '../common/read-notification'; const log = debug('misskey'); @@ -45,6 +46,11 @@ export default function homeStream(request: websocket.request, connection: webso }); break; + case 'read_notification': + if (!msg.id) return; + readNotification(user._id, msg.id); + break; + case 'capture': if (!msg.id) return; const postId = msg.id; diff --git a/src/api/streaming.ts b/src/api/streaming.ts index db600013b..0e512fb21 100644 --- a/src/api/streaming.ts +++ b/src/api/streaming.ts @@ -9,6 +9,7 @@ import isNativeToken from './common/is-native-token'; import homeStream from './stream/home'; import messagingStream from './stream/messaging'; import serverStream from './stream/server'; +import channelStream from './stream/channel'; module.exports = (server: http.Server) => { /** @@ -26,14 +27,6 @@ module.exports = (server: http.Server) => { return; } - const user = await authenticate(request.resourceURL.query.i); - - if (user == null) { - connection.send('authentication-failed'); - connection.close(); - return; - } - // Connect to Redis const subscriber = redis.createClient( config.redis.port, config.redis.host); @@ -43,6 +36,19 @@ module.exports = (server: http.Server) => { subscriber.quit(); }); + if (request.resourceURL.pathname === '/channel') { + channelStream(request, connection, subscriber); + return; + } + + const user = await authenticate(request.resourceURL.query.i); + + if (user == null) { + connection.send('authentication-failed'); + connection.close(); + return; + } + const channel = request.resourceURL.pathname === '/' ? homeStream : request.resourceURL.pathname === '/messaging' ? messagingStream : diff --git a/src/common/get-post-summary.ts b/src/common/get-post-summary.ts index f628a32b4..6e8f65708 100644 --- a/src/common/get-post-summary.ts +++ b/src/common/get-post-summary.ts @@ -3,7 +3,13 @@ * @param {*} post 投稿 */ const summarize = (post: any): string => { - let summary = post.text ? post.text : ''; + let summary = ''; + + // チャンネル + summary += post.channel ? `${post.channel.title}:` : ''; + + // 本文 + summary += post.text ? post.text : ''; // メディアが添付されているとき if (post.media) { @@ -16,9 +22,9 @@ const summarize = (post: any): string => { } // 返信のとき - if (post.reply_to_id) { - if (post.reply_to) { - summary += ` RE: ${summarize(post.reply_to)}`; + if (post.reply_id) { + if (post.reply) { + summary += ` RE: ${summarize(post.reply)}`; } else { summary += ' RE: ...'; } diff --git a/src/config.ts b/src/config.ts index 46a93f5fe..d37d227a4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -88,6 +88,7 @@ type Mixin = { api_url: string; auth_url: string; about_url: string; + ch_url: string; stats_url: string; status_url: string; dev_url: string; @@ -122,6 +123,7 @@ export default function load() { mixin.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://')); mixin.api_url = `${mixin.scheme}://api.${mixin.host}`; mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`; + mixin.ch_url = `${mixin.scheme}://ch.${mixin.host}`; mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`; mixin.about_url = `${mixin.scheme}://about.${mixin.host}`; mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`; diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts index 6ee7f4534..c978e6460 100644 --- a/src/db/mongodb.ts +++ b/src/db/mongodb.ts @@ -1,11 +1,38 @@ -import * as mongo from 'monk'; - import config from '../conf'; const uri = config.mongodb.user && config.mongodb.pass - ? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}` - : `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; +? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}` +: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; + +/** + * monk + */ +import * as mongo from 'monk'; const db = mongo(uri); export default db; + +/** + * MongoDB native module (officialy) + */ +import * as mongodb from 'mongodb'; + +let mdb: mongodb.Db; + +const nativeDbConn = async (): Promise => { + if (mdb) return mdb; + + const db = await ((): Promise => new Promise((resolve, reject) => { + mongodb.MongoClient.connect(uri, (e, db) => { + if (e) return reject(e); + resolve(db); + }); + }))(); + + mdb = db; + + return db; +}; + +export { nativeDbConn }; diff --git a/src/docs/api/entities/post.pug b/src/docs/api/entities/post.pug index e505d3fcb..954f17271 100644 --- a/src/docs/api/entities/post.pug +++ b/src/docs/api/entities/post.pug @@ -52,11 +52,11 @@ block content td Number td 返信数 tr.optional - td reply_to + td reply td: a(href='./post', target='_blank') Post td 返信先の投稿 tr.nullable - td reply_to_id + td reply_id td ID td 返信先の投稿のID tr.optional @@ -90,7 +90,7 @@ block content { "created_at": "2016-12-10T00:28:50.114Z", "media_ids": null, - "reply_to_id": "584a16b15860fc52320137e3", + "reply_id": "584a16b15860fc52320137e3", "repost_id": null, "text": "小日向美穂だぞ!", "user_id": "5848bf7764e572683f4402f8", @@ -117,10 +117,10 @@ block content "is_following": true, "is_followed": true }, - "reply_to": { + "reply": { "created_at": "2016-12-09T02:28:01.563Z", "media_ids": null, - "reply_to_id": "5849d35e547e4249be329884", + "reply_id": "5849d35e547e4249be329884", "repost_id": null, "text": "アイコン小日向美穂?", "user_id": "57d01a501fdf2d07be417afe", diff --git a/src/file/server.ts b/src/file/server.ts index ee67cf786..375f29487 100644 --- a/src/file/server.ts +++ b/src/file/server.ts @@ -9,7 +9,7 @@ import * as cors from 'cors'; import * as mongodb from 'mongodb'; import * as gm from 'gm'; -import File from '../api/models/drive-file'; +import DriveFile, { getGridFSBucket } from '../api/models/drive-file'; /** * Init app @@ -97,17 +97,28 @@ app.get('/:id', async (req, res) => { return; } - const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) }); + const fileId = new mongodb.ObjectID(req.params.id); + const file = await DriveFile.findOne({ _id: fileId }); if (file == null) { - res.status(404).sendFile(`${__dirname} / assets / dummy.png`); - return; - } else if (file.data == null) { - res.sendStatus(400); + res.status(404).sendFile(`${__dirname}/assets/dummy.png`); return; } - send(file.data.buffer, file.type, req, res); + const bucket = await getGridFSBucket(); + + const buffer = await ((id): Promise => new Promise((resolve, reject) => { + const chunks = []; + const readableStream = bucket.openDownloadStream(id); + readableStream.on('data', chunk => { + chunks.push(chunk); + }); + readableStream.on('end', () => { + resolve(Buffer.concat(chunks)); + }); + }))(fileId); + + send(buffer, file.metadata.type, req, res); }); app.get('/:id/:name', async (req, res) => { @@ -117,17 +128,28 @@ app.get('/:id/:name', async (req, res) => { return; } - const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) }); + const fileId = new mongodb.ObjectID(req.params.id); + const file = await DriveFile.findOne({ _id: fileId }); if (file == null) { res.status(404).sendFile(`${__dirname}/assets/dummy.png`); return; - } else if (file.data == null) { - res.sendStatus(400); - return; } - send(file.data.buffer, file.type, req, res); + const bucket = await getGridFSBucket(); + + const buffer = await ((id): Promise => new Promise((resolve, reject) => { + const chunks = []; + const readableStream = bucket.openDownloadStream(id); + readableStream.on('data', chunk => { + chunks.push(chunk); + }); + readableStream.on('end', () => { + resolve(Buffer.concat(chunks)); + }); + }))(fileId); + + send(buffer, file.metadata.type, req, res); }); module.exports = app; diff --git a/src/web/app/base.styl b/src/web/app/app.styl similarity index 94% rename from src/web/app/base.styl rename to src/web/app/app.styl index 81c039f0a..94faba73d 100644 --- a/src/web/app/base.styl +++ b/src/web/app/app.styl @@ -5,8 +5,6 @@ json('../../const.json') $theme-color = themeColor $theme-color-foreground = themeColorForeground -@import './reset' - /* ::selection background $theme-color @@ -14,6 +12,9 @@ $theme-color-foreground = themeColorForeground */ * + position relative + box-sizing border-box + background-clip padding-box !important tap-highlight-color rgba($theme-color, 0.7) -webkit-tap-highlight-color rgba($theme-color, 0.7) @@ -29,6 +30,9 @@ html &, * cursor progress !important +body + overflow-wrap break-word + #error padding 32px color #fff diff --git a/src/web/app/auth/style.styl b/src/web/app/auth/style.styl index 046a5ff6e..bd25e1b57 100644 --- a/src/web/app/auth/style.styl +++ b/src/web/app/auth/style.styl @@ -1,4 +1,5 @@ -@import "../base" +@import "../app" +@import "../reset" html background #eee diff --git a/src/web/app/ch/router.js b/src/web/app/ch/router.js new file mode 100644 index 000000000..424158f40 --- /dev/null +++ b/src/web/app/ch/router.js @@ -0,0 +1,32 @@ +import * as riot from 'riot'; +const route = require('page'); +let page = null; + +export default me => { + route('/', index); + route('/:channel', channel); + route('*', notFound); + + function index() { + mount(document.createElement('mk-index')); + } + + function channel(ctx) { + const el = document.createElement('mk-channel'); + el.setAttribute('id', ctx.params.channel); + mount(el); + } + + function notFound() { + mount(document.createElement('mk-not-found')); + } + + // EXEC + route(); +}; + +function mount(content) { + if (page) page.unmount(); + const body = document.getElementById('app'); + page = riot.mount(body.appendChild(content))[0]; +} diff --git a/src/web/app/ch/script.js b/src/web/app/ch/script.js new file mode 100644 index 000000000..760d405c5 --- /dev/null +++ b/src/web/app/ch/script.js @@ -0,0 +1,18 @@ +/** + * Channels + */ + +// Style +import './style.styl'; + +require('./tags'); +import init from '../init'; +import route from './router'; + +/** + * init + */ +init(me => { + // Start routing + route(me); +}); diff --git a/src/web/app/ch/style.styl b/src/web/app/ch/style.styl new file mode 100644 index 000000000..21ca648cb --- /dev/null +++ b/src/web/app/ch/style.styl @@ -0,0 +1,10 @@ +@import "../app" + +html + padding 8px + background #efefef + +#wait + top auto + bottom 15px + left 15px diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag new file mode 100644 index 000000000..4ae62e7b3 --- /dev/null +++ b/src/web/app/ch/tags/channel.tag @@ -0,0 +1,403 @@ + + +
+
+

{ channel.title }

+ +
+

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

+

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

+
+ + + +
+

読み込み中

+
+

まだ投稿がありません

+ + + +
+
+
+ +
+

参加するにはログインまたは新規登録してください

+
+
+
+ Misskey ver { version } (葵 aoi) +
+
+ + +
+ + +
+ { post.index }: + { post.user.name } + + + ID:{ post.user.username } +
+
+ >>{ post.reply.index } + { post.text } +
+ + + { + + +
+
+ + +
+ + +

>>{ reply.index } ({ reply.user.name }): [x]

+ +
+ + + +
+ +
    +
  1. { name }
  2. +
+ + + +
+ + + + + + + + + + diff --git a/src/web/app/ch/tags/header.tag b/src/web/app/ch/tags/header.tag new file mode 100644 index 000000000..5cdcbd09c --- /dev/null +++ b/src/web/app/ch/tags/header.tag @@ -0,0 +1,20 @@ + +
+ Index | Misskey +
+ + + +
diff --git a/src/web/app/ch/tags/index.js b/src/web/app/ch/tags/index.js new file mode 100644 index 000000000..12ffdaeb8 --- /dev/null +++ b/src/web/app/ch/tags/index.js @@ -0,0 +1,3 @@ +require('./index.tag'); +require('./channel.tag'); +require('./header.tag'); diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag new file mode 100644 index 000000000..50ccc0d91 --- /dev/null +++ b/src/web/app/ch/tags/index.tag @@ -0,0 +1,35 @@ + + +
+ +
+ + + +
diff --git a/src/web/app/common/scripts/channel-stream.js b/src/web/app/common/scripts/channel-stream.js new file mode 100644 index 000000000..17944dbe4 --- /dev/null +++ b/src/web/app/common/scripts/channel-stream.js @@ -0,0 +1,16 @@ +'use strict'; + +import Stream from './stream'; + +/** + * Channel stream connection + */ +class Connection extends Stream { + constructor(channelId) { + super('channel', { + channel: channelId + }); + } +} + +export default Connection; diff --git a/src/web/app/common/scripts/config.js b/src/web/app/common/scripts/config.js index 75a7abba2..c5015622f 100644 --- a/src/web/app/common/scripts/config.js +++ b/src/web/app/common/scripts/config.js @@ -6,6 +6,7 @@ const host = isRoot ? Url.host : Url.host.substring(Url.host.indexOf('.') + 1, U const scheme = Url.protocol; const url = `${scheme}//${host}`; const apiUrl = `${scheme}//api.${host}`; +const chUrl = `${scheme}//ch.${host}`; const devUrl = `${scheme}//dev.${host}`; const aboutUrl = `${scheme}//about.${host}`; const statsUrl = `${scheme}//stats.${host}`; @@ -16,6 +17,7 @@ export default { scheme, url, apiUrl, + chUrl, devUrl, aboutUrl, statsUrl, diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js index afa8a2dce..977e3fa9a 100644 --- a/src/web/app/desktop/router.js +++ b/src/web/app/desktop/router.js @@ -7,14 +7,15 @@ const route = require('page'); let page = null; export default me => { - route('/', index); - route('/i>mentions', mentions); - route('/post::post', post); - route('/search::query', search); - route('/:user', user.bind(null, 'home')); - route('/:user/graphs', user.bind(null, 'graphs')); - route('/:user/:post', post); - route('*', notFound); + route('/', index); + route('/selectdrive', selectDrive); + route('/i>mentions', mentions); + route('/post::post', post); + route('/search::query', search); + route('/:user', user.bind(null, 'home')); + route('/:user/graphs', user.bind(null, 'graphs')); + route('/:user/:post', post); + route('*', notFound); function index() { me ? home() : entrance(); @@ -54,6 +55,10 @@ export default me => { mount(el); } + function selectDrive() { + mount(document.createElement('mk-selectdrive-page')); + } + function notFound() { mount(document.createElement('mk-not-found')); } @@ -67,6 +72,7 @@ export default me => { }; function mount(content) { + document.documentElement.style.background = '#313a42'; document.documentElement.removeAttribute('data-page'); if (page) page.unmount(); const body = document.getElementById('app'); diff --git a/src/web/app/desktop/style.styl b/src/web/app/desktop/style.styl index 88adb68b2..4597dffdb 100644 --- a/src/web/app/desktop/style.styl +++ b/src/web/app/desktop/style.styl @@ -1,4 +1,5 @@ -@import "../base" +@import "../app" +@import "../reset" @import "../../../../node_modules/cropperjs/dist/cropper.css" *::input-placeholder diff --git a/src/web/app/desktop/tags/home-widgets/rss-reader.tag b/src/web/app/desktop/tags/home-widgets/rss-reader.tag index 550d7e76d..e9b740762 100644 --- a/src/web/app/desktop/tags/home-widgets/rss-reader.tag +++ b/src/web/app/desktop/tags/home-widgets/rss-reader.tag @@ -4,7 +4,7 @@ -

読み込んでいます

+

%i18n:common.loading%

+ + diff --git a/src/web/app/desktop/tags/pages/user.tag b/src/web/app/desktop/tags/pages/user.tag index 864fe2273..811ca5c0f 100644 --- a/src/web/app/desktop/tags/pages/user.tag +++ b/src/web/app/desktop/tags/pages/user.tag @@ -16,7 +16,7 @@ this.refs.ui.refs.user.on('user-fetched', user => { Progress.set(0.5); - document.title = user.name + ' | Misskey' + document.title = user.name + ' | Misskey'; }); this.refs.ui.refs.user.on('loaded', () => { diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag index 58343482d..ce7f81e32 100644 --- a/src/web/app/desktop/tags/post-detail.tag +++ b/src/web/app/desktop/tags/post-detail.tag @@ -1,6 +1,6 @@
- @@ -9,8 +9,8 @@
-
- +
+

@@ -329,7 +329,7 @@ // Fetch context this.api('posts/context', { - post_id: this.p.reply_to_id + post_id: this.p.reply_id }).then(context => { this.update({ contextFetching: false, diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag index 6a363d67c..5041078be 100644 --- a/src/web/app/desktop/tags/post-form.tag +++ b/src/web/app/desktop/tags/post-form.tag @@ -475,7 +475,7 @@ this.api('posts/create', { text: this.refs.text.value == '' ? undefined : this.refs.text.value, media_ids: files, - reply_to_id: this.inReplyToPost ? this.inReplyToPost.id : undefined, + reply_id: this.inReplyToPost ? this.inReplyToPost.id : undefined, repost_id: this.repost ? this.repost.id : undefined, poll: this.poll ? this.refs.poll.get() : undefined }).then(data => { diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag index 02cb5251b..c75ae2911 100644 --- a/src/web/app/desktop/tags/sub-post-content.tag +++ b/src/web/app/desktop/tags/sub-post-content.tag @@ -1,6 +1,6 @@

- + diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag index 2d6b439e3..44f3d5d8e 100644 --- a/src/web/app/desktop/tags/timeline.tag +++ b/src/web/app/desktop/tags/timeline.tag @@ -82,8 +82,8 @@ -
- +
+

@@ -112,7 +112,8 @@

- +

{ p.channel.title }:

+

@@ -333,6 +334,9 @@ font-weight 400 font-style normal + > .channel + margin 0 + > .reply margin-right 8px color #717171 diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag index e0d7393b0..3123c34f4 100644 --- a/src/web/app/desktop/tags/ui.tag +++ b/src/web/app/desktop/tags/ui.tag @@ -319,18 +319,26 @@ -
    -
  • - - -

    %i18n:desktop.tags.mk-ui-header-nav.home%

    -
    -
  • -
  • - - -

    %i18n:desktop.tags.mk-ui-header-nav.messaging%

    - +
      + +
    • + + +

      %i18n:desktop.tags.mk-ui-header-nav.home%

      +
      +
    • +
    • + + +

      %i18n:desktop.tags.mk-ui-header-nav.messaging%

      + +
      +
    • + +
    • + + +

      %i18n:desktop.tags.mk-ui-header-nav.ch%

    • diff --git a/src/web/app/dev/style.styl b/src/web/app/dev/style.styl index 4fd537709..cdbcb0e26 100644 --- a/src/web/app/dev/style.styl +++ b/src/web/app/dev/style.styl @@ -1,4 +1,5 @@ -@import "../base" +@import "../app" +@import "../reset" html background-color #fff diff --git a/src/web/app/init.js b/src/web/app/init.js index cb661c259..5a6899ed4 100644 --- a/src/web/app/init.js +++ b/src/web/app/init.js @@ -21,6 +21,11 @@ require('./common/tags'); console.info(`Misskey v${VERSION} (葵 aoi)`); +{ // Set lang attr + const html = document.documentElement; + html.setAttribute('lang', LANG); +} + { // Set description meta tag const head = document.getElementsByTagName('head')[0]; const meta = document.createElement('meta'); diff --git a/src/web/app/mobile/router.js b/src/web/app/mobile/router.js index d59b2ec3a..01eb3c814 100644 --- a/src/web/app/mobile/router.js +++ b/src/web/app/mobile/router.js @@ -8,6 +8,7 @@ let page = null; export default me => { route('/', index); + route('/selectdrive', selectDrive); route('/i/notifications', notifications); route('/i/messaging', messaging); route('/i/messaging/:username', messaging); @@ -122,6 +123,10 @@ export default me => { mount(el); } + function selectDrive() { + mount(document.createElement('mk-selectdrive-page')); + } + function notFound() { mount(document.createElement('mk-not-found')); } diff --git a/src/web/app/mobile/style.styl b/src/web/app/mobile/style.styl index bd6965e40..63e4f2349 100644 --- a/src/web/app/mobile/style.styl +++ b/src/web/app/mobile/style.styl @@ -1,4 +1,5 @@ -@import "../base" +@import "../app" +@import "../reset" #wait top auto diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag index 9f3e64773..6929c50ab 100644 --- a/src/web/app/mobile/tags/drive.tag +++ b/src/web/app/mobile/tags/drive.tag @@ -1,5 +1,5 @@ -
-
- +
+

@@ -348,7 +348,7 @@ // Fetch context this.api('posts/context', { - post_id: this.p.reply_to_id + post_id: this.p.reply_id }).then(context => { this.update({ contextFetching: false, diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag index cf267de94..d7d382c9e 100644 --- a/src/web/app/mobile/tags/post-form.tag +++ b/src/web/app/mobile/tags/post-form.tag @@ -267,7 +267,7 @@ this.api('posts/create', { text: this.refs.text.value == '' ? undefined : this.refs.text.value, media_ids: files, - reply_to_id: opts.reply ? opts.reply.id : undefined, + reply_id: opts.reply ? opts.reply.id : undefined, poll: this.poll ? this.refs.poll.get() : undefined }).then(data => { this.trigger('post'); diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag index 97e0ecec0..e32e24518 100644 --- a/src/web/app/mobile/tags/sub-post-content.tag +++ b/src/web/app/mobile/tags/sub-post-content.tag @@ -1,5 +1,5 @@ -

+
({ post.media.length }個のメディア) diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag index c7f5bfd68..f9ec2cca6 100644 --- a/src/web/app/mobile/tags/timeline.tag +++ b/src/web/app/mobile/tags/timeline.tag @@ -137,8 +137,8 @@ -
- +
+

@@ -164,7 +164,8 @@

- +

{ p.channel.title }:

+

@@ -373,6 +374,9 @@ mk-url-preview margin-top 8px + > .channel + margin 0 + > .reply margin-right 8px color #717171 diff --git a/src/web/app/mobile/tags/ui-header.tag b/src/web/app/mobile/tags/ui-header.tag deleted file mode 100644 index 10b44b215..000000000 --- a/src/web/app/mobile/tags/ui-header.tag +++ /dev/null @@ -1,156 +0,0 @@ - - -
-
-
- - -

Misskey

- -
-
- - -
diff --git a/src/web/app/mobile/tags/ui-nav.tag b/src/web/app/mobile/tags/ui-nav.tag deleted file mode 100644 index 34235ba4f..000000000 --- a/src/web/app/mobile/tags/ui-nav.tag +++ /dev/null @@ -1,170 +0,0 @@ - -
- - - -
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag index 9d9cd4d74..b2d96f6b8 100644 --- a/src/web/app/mobile/tags/ui.tag +++ b/src/web/app/mobile/tags/ui.tag @@ -30,9 +30,378 @@ }; this.onStreamNotification = notification => { + // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない + this.stream.send({ + type: 'read_notification', + id: notification.id + }); + riot.mount(document.body.appendChild(document.createElement('mk-notify')), { notification: notification }); }; + + + +
+
+
+ + +

Misskey

+ +
+
+ + +
+ + +
+ + + +
diff --git a/src/web/app/reset.styl b/src/web/app/reset.styl index 85bbd1147..3d4b06dbd 100644 --- a/src/web/app/reset.styl +++ b/src/web/app/reset.styl @@ -1,16 +1,3 @@ -* - position relative - box-sizing border-box - background-clip padding-box !important - -html -body - margin 0 - padding 0 - -body - overflow-wrap break-word - input:not([type]) input[type='text'] input[type='password'] diff --git a/src/web/app/safe.js b/src/web/app/safe.js index c5fbb83a9..77293be81 100644 --- a/src/web/app/safe.js +++ b/src/web/app/safe.js @@ -7,5 +7,8 @@ if (!('fetch' in window)) { alert( 'お使いのブラウザが古いためMisskeyを動作させることができません。' + - 'バージョンを最新のものに更新するか、別のブラウザをお試しください。'); + 'バージョンを最新のものに更新するか、別のブラウザをお試しください。' + + '\n\n' + + 'Your browser seems outdated.' + + 'To run Misskey, please update your browser to latest version or try other browsers.'); } diff --git a/src/web/app/stats/style.styl b/src/web/app/stats/style.styl index b48d7aeb9..5ae230ea5 100644 --- a/src/web/app/stats/style.styl +++ b/src/web/app/stats/style.styl @@ -1,4 +1,5 @@ -@import "../base" +@import "../app" +@import "../reset" html color #456267 diff --git a/src/web/app/status/style.styl b/src/web/app/status/style.styl index b48d7aeb9..5ae230ea5 100644 --- a/src/web/app/status/style.styl +++ b/src/web/app/status/style.styl @@ -1,4 +1,5 @@ -@import "../base" +@import "../app" +@import "../reset" html color #456267 diff --git a/test/api.js b/test/api.js index 1e731b554..c0da9d6c5 100644 --- a/test/api.js +++ b/test/api.js @@ -277,15 +277,15 @@ describe('API', () => { const me = await insertSakurako(); const post = { text: 'さく', - reply_to_id: himaPost._id.toString() + reply_id: himaPost._id.toString() }; const res = await request('/posts/create', post, me); res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('text').eql(post.text); - res.body.should.have.property('reply_to_id').eql(post.reply_to_id); - res.body.should.have.property('reply_to'); - res.body.reply_to.should.have.property('text').eql(himaPost.text); + res.body.should.have.property('reply_id').eql(post.reply_id); + res.body.should.have.property('reply'); + res.body.reply.should.have.property('text').eql(himaPost.text); })); it('repostできる', async(async () => { @@ -350,7 +350,7 @@ describe('API', () => { const me = await insertSakurako(); const post = { text: 'さく', - reply_to_id: '000000000000000000000000' + reply_id: '000000000000000000000000' }; const res = await request('/posts/create', post, me); res.should.have.status(400); @@ -369,7 +369,7 @@ describe('API', () => { const me = await insertSakurako(); const post = { text: 'さく', - reply_to_id: 'kyoppie' + reply_id: 'kyoppie' }; const res = await request('/posts/create', post, me); res.should.have.status(400); @@ -1152,9 +1152,12 @@ async function insertHimawari(opts) { } async function insertDriveFile(opts) { - return await db.get('drive_files').insert(Object.assign({ - name: 'strawberry-pasta.png' - }, opts)); + return await db.get('drive_files.files').insert({ + length: opts.datasize, + metadata: Object.assign({ + name: 'strawberry-pasta.png' + }, opts) + }); } async function insertDriveFolder(opts) { diff --git a/tools/migration/reply_to-to-reply.js b/tools/migration/reply_to-to-reply.js new file mode 100644 index 000000000..ceb272ebc --- /dev/null +++ b/tools/migration/reply_to-to-reply.js @@ -0,0 +1,5 @@ +db.posts.update({}, { + $rename: { + reply_to_id: 'reply_id' + } +}, false, true); diff --git a/tools/migration/use-gridfs.js b/tools/migration/use-gridfs.js new file mode 100644 index 000000000..d41514416 --- /dev/null +++ b/tools/migration/use-gridfs.js @@ -0,0 +1,49 @@ +// for Node.js interpret + +const { default: db } = require('../../built/db/mongodb') +const { default: DriveFile, getGridFSBucket } = require('../../built/api/models/drive-file') +const { Duplex } = require('stream') + +const writeToGridFS = (bucket, buffer, ...rest) => new Promise((resolve, reject) => { + const writeStream = bucket.openUploadStreamWithId(...rest) + + const dataStream = new Duplex() + dataStream.push(buffer) + dataStream.push(null) + + writeStream.once('finish', resolve) + writeStream.on('error', reject) + + dataStream.pipe(writeStream) +}) + +const migrateToGridFS = async (doc) => { + const id = doc._id + const buffer = doc.data.buffer + const created_at = doc.created_at + + delete doc._id + delete doc.created_at + delete doc.datasize + delete doc.hash + delete doc.data + + const bucket = await getGridFSBucket() + const added = await writeToGridFS(bucket, buffer, id, `${id}/${doc.name}`, { metadata: doc }) + + const result = await DriveFile.update(id, { + $set: { + uploadDate: created_at + } + }) + + return added && result.ok === 1 +} + +const main = async () => { + const docs = await db.get('drive_files').find() + const all = await Promise.all(docs.map(migrateToGridFS)) + return all +} + +main().then(console.dir).catch(console.error) diff --git a/webpack/plugins/const.ts b/webpack/plugins/const.ts index ccfcb4526..f64160b01 100644 --- a/webpack/plugins/const.ts +++ b/webpack/plugins/const.ts @@ -7,7 +7,8 @@ import * as webpack from 'webpack'; import version from '../../src/version'; const constants = require('../../src/const.json'); -export default () => new webpack.DefinePlugin({ +export default lang => new webpack.DefinePlugin({ VERSION: JSON.stringify(version), + LANG: JSON.stringify(lang), THEME_COLOR: JSON.stringify(constants.themeColor) }); diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts index d5191f155..345af7df9 100644 --- a/webpack/plugins/index.ts +++ b/webpack/plugins/index.ts @@ -8,9 +8,9 @@ import banner from './banner'; const env = process.env.NODE_ENV; const isProduction = env === 'production'; -export default version => { +export default (version, lang) => { const plugins = [ - constant(), + constant(lang), new StringReplacePlugin(), hoist() ]; diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts index 5199285d5..97782a410 100644 --- a/webpack/webpack.config.ts +++ b/webpack/webpack.config.ts @@ -16,6 +16,7 @@ module.exports = langs.map(([lang, locale]) => { const entry = { desktop: './src/web/app/desktop/script.js', mobile: './src/web/app/mobile/script.js', + ch: './src/web/app/ch/script.js', stats: './src/web/app/stats/script.js', status: './src/web/app/status/script.js', dev: './src/web/app/dev/script.js', @@ -31,7 +32,7 @@ module.exports = langs.map(([lang, locale]) => { name, entry, module: module_(lang, locale), - plugins: plugins(version), + plugins: plugins(version, lang), output }; });