diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2018-10-08 15:37:24 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-10-08 15:37:24 +0900 |
| commit | 9c170c426be01773afb15a9868ff3c278e09409c (patch) | |
| tree | 0229bb52dd9197308d193f4e41bbc11d3dcb95a1 /src/server/api | |
| parent | New translations ja-JP.yml (Norwegian) (diff) | |
| parent | fix(package): update @types/mongodb to version 3.1.10 (#2849) (diff) | |
| download | misskey-9c170c426be01773afb15a9868ff3c278e09409c.tar.gz misskey-9c170c426be01773afb15a9868ff3c278e09409c.tar.bz2 misskey-9c170c426be01773afb15a9868ff3c278e09409c.zip | |
Merge branch 'develop' into l10n_develop
Diffstat (limited to 'src/server/api')
87 files changed, 2273 insertions, 1307 deletions
diff --git a/src/server/api/call.ts b/src/server/api/call.ts index e9abc11f54..7419bdc95d 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -9,6 +9,10 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any) const ep = endpoints.find(e => e.name === endpoint); + if (ep == null) { + return rej('ENDPOINT_NOT_FOUND'); + } + if (ep.meta.secure && !isSecure) { return rej('ACCESS_DENIED'); } @@ -25,10 +29,8 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any) return rej('YOU_ARE_NOT_ADMIN'); } - if (app && ep.meta.kind) { - if (!app.permission.some(p => p === ep.meta.kind)) { - return rej('PERMISSION_DENIED'); - } + if (app && ep.meta.kind && !app.permission.some(p => p === ep.meta.kind)) { + return rej('PERMISSION_DENIED'); } if (ep.meta.requireCredential && ep.meta.limit) { diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts index 005240a37c..075e369832 100644 --- a/src/server/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -1,7 +1,7 @@ import * as mongo from 'mongodb'; import Message from '../../../models/messaging-message'; import { IMessagingMessage as IMessage } from '../../../models/messaging-message'; -import { publishUserStream } from '../../../stream'; +import { publishMainStream } from '../../../stream'; import { publishMessagingStream } from '../../../stream'; import { publishMessagingIndexStream } from '../../../stream'; import User from '../../../models/user'; @@ -71,6 +71,6 @@ export default ( }); // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 - publishUserStream(userId, 'read_all_messaging_messages'); + publishMainStream(userId, 'readAllMessagingMessages'); } }); diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts index 0b0f3e4e5a..2d58ada4ce 100644 --- a/src/server/api/common/read-notification.ts +++ b/src/server/api/common/read-notification.ts @@ -1,6 +1,6 @@ import * as mongo from 'mongodb'; import { default as Notification, INotification } from '../../../models/notification'; -import { publishUserStream } from '../../../stream'; +import { publishMainStream } from '../../../stream'; import Mute from '../../../models/mute'; import User from '../../../models/user'; @@ -66,6 +66,6 @@ export default ( }); // 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行 - publishUserStream(userId, 'read_all_notifications'); + publishMainStream(userId, 'readAllNotifications'); } }); diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index d4a44070e6..2b00094269 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -79,7 +79,7 @@ const files = glob.sync('**/*.js', { }); const endpoints: IEndpoint[] = files.map(f => { - const ep = require('./endpoints/' + f); + const ep = require(`./endpoints/${f}`); return { name: f.replace('.js', ''), diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index 2c7929fabe..f0ebfbe936 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -11,11 +11,35 @@ export const meta = { requireAdmin: true, params: { + broadcasts: $.arr($.obj()).optional.nullable.note({ + desc: { + 'ja-JP': 'ブロードキャスト' + } + }), + disableRegistration: $.bool.optional.nullable.note({ desc: { 'ja-JP': '招待制か否か' } }), + + disableLocalTimeline: $.bool.optional.nullable.note({ + desc: { + 'ja-JP': 'ローカルタイムライン(とソーシャルタイムライン)を無効にするか否か' + } + }), + + hidedTags: $.arr($.str).optional.nullable.note({ + desc: { + 'ja-JP': '統計などで無視するハッシュタグ' + } + }), + + bannerUrl: $.str.optional.nullable.note({ + desc: { + 'ja-JP': 'インスタンスのバナー画像URL' + } + }), } }; @@ -25,10 +49,26 @@ export default (params: any) => new Promise(async (res, rej) => { const set = {} as any; - if (ps.disableRegistration === true || ps.disableRegistration === false) { + if (ps.broadcasts) { + set.broadcasts = ps.broadcasts; + } + + if (typeof ps.disableRegistration === 'boolean') { set.disableRegistration = ps.disableRegistration; } + if (typeof ps.disableLocalTimeline === 'boolean') { + set.disableLocalTimeline = ps.disableLocalTimeline; + } + + if (Array.isArray(ps.hidedTags)) { + set.hidedTags = ps.hidedTags; + } + + if (ps.bannerUrl !== undefined) { + set.bannerUrl = ps.bannerUrl; + } + await Meta.update({}, { $set: set }, { upsert: true }); diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts new file mode 100644 index 0000000000..ffeafb2538 --- /dev/null +++ b/src/server/api/endpoints/aggregation/hashtags.ts @@ -0,0 +1,66 @@ +import Note from '../../../../models/note'; +import Meta from '../../../../models/meta'; + +export default () => new Promise(async (res, rej) => { + const meta = await Meta.findOne({}); + const hidedTags = meta ? (meta.hidedTags || []).map(t => t.toLowerCase()) : []; + + const span = 1000 * 60 * 60 * 24 * 7; // 1週間 + + //#region 1. 指定期間の内に投稿されたハッシュタグ(とユーザーのペア)を集計 + const data = await Note.aggregate([{ + $match: { + createdAt: { + $gt: new Date(Date.now() - span) + }, + tagsLower: { + $exists: true, + $ne: [] + } + } + }, { + $unwind: '$tagsLower' + }, { + $group: { + _id: { tag: '$tagsLower', userId: '$userId' } + } + }]) as Array<{ + _id: { + tag: string; + userId: any; + } + }>; + //#endregion + + if (data.length == 0) { + return res([]); + } + + let tags: Array<{ + name: string; + count: number; + }> = []; + + // カウント + data.map(x => x._id).forEach(x => { + // ブラックリストに登録されているタグなら弾く + if (hidedTags.includes(x.tag)) return; + + const i = tags.findIndex(tag => tag.name == x.tag); + if (i != -1) { + tags[i].count++; + } else { + tags.push({ + name: x.tag, + count: 1 + }); + } + }); + + // タグを人気順に並べ替え + tags = tags.sort((a, b) => b.count - a.count); + + tags = tags.slice(0, 30); + + res(tags); +}); diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts new file mode 100644 index 0000000000..1f390d01aa --- /dev/null +++ b/src/server/api/endpoints/ap/show.ts @@ -0,0 +1,116 @@ +import $ from 'cafy'; +import getParams from '../../get-params'; +import config from '../../../../config'; +import * as mongo from 'mongodb'; +import User, { pack as packUser, IUser } from '../../../../models/user'; +import { createPerson } from '../../../../remote/activitypub/models/person'; +import Note, { pack as packNote, INote } from '../../../../models/note'; +import { createNote } from '../../../../remote/activitypub/models/note'; +import Resolver from '../../../../remote/activitypub/resolver'; + +export const meta = { + desc: { + 'ja-JP': 'URIを指定してActivityPubオブジェクトを参照します。' + }, + + requireCredential: false, + + params: { + uri: $.str.note({ + desc: { + 'ja-JP': 'ActivityPubオブジェクトのURI' + } + }), + }, +}; + +export default (params: any) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + const object = await fetchAny(ps.uri); + if (object !== null) return res(object); + + return rej('object not found'); +}); + +/*** + * URIからUserかNoteを解決する + */ +async function fetchAny(uri: string) { + // URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ + if (uri.startsWith(config.url + '/')) { + const id = new mongo.ObjectID(uri.split('/').pop()); + const [ user, note ] = await Promise.all([ + User.findOne({ _id: id }), + Note.findOne({ _id: id }) + ]); + + const packed = await mergePack(user, note); + if (packed !== null) return packed; + } + + // URI(AP Object id)としてDB検索 + { + const [ user, note ] = await Promise.all([ + User.findOne({ uri: uri }), + Note.findOne({ uri: uri }) + ]); + + const packed = await mergePack(user, note); + if (packed !== null) return packed; + } + + // リモートから一旦オブジェクトフェッチ + const resolver = new Resolver(); + const object = await resolver.resolve(uri) as any; + + // /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する + // これはDBに存在する可能性があるため再度DB検索 + if (uri !== object.id) { + const [ user, note ] = await Promise.all([ + User.findOne({ uri: object.id }), + Note.findOne({ uri: object.id }) + ]); + + const packed = await mergePack(user, note); + if (packed !== null) return packed; + } + + // それでもみつからなければ新規であるため登録 + if (object.type === 'Person') { + const user = await createPerson(object.id); + return { + type: 'User', + object: user + }; + } + + if (object.type === 'Note') { + const note = await createNote(object.id); + return { + type: 'Note', + object: note + }; + } + + return null; +} + +async function mergePack(user: IUser, note: INote) { + if (user !== null) { + return { + type: 'User', + object: await packUser(user, null, { detail: true }) + }; + } + + if (note !== null) { + return { + type: 'Note', + object: await packNote(note, null, { detail: true }) + }; + } + + return null; +} diff --git a/src/server/api/endpoints/chart.ts b/src/server/api/endpoints/chart.ts index 7da970131e..3b1a3b56fc 100644 --- a/src/server/api/endpoints/chart.ts +++ b/src/server/api/endpoints/chart.ts @@ -6,6 +6,15 @@ type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; function migrateStats(stats: IStats[]) { stats.forEach(stat => { + if (stat.network == null) { + stat.network = { + requests: 0, + totalTime: 0, + incomingBytes: 0, + outgoingBytes: 0 + }; + } + const isOldData = stat.users.local.inc == null || stat.users.local.dec == null || @@ -180,6 +189,12 @@ export default (params: any) => new Promise(async (res, rej) => { decCount: 0, decSize: 0 } + }, + network: { + requests: 0, + totalTime: 0, + incomingBytes: 0, + outgoingBytes: 0 } }); } else { @@ -236,6 +251,12 @@ export default (params: any) => new Promise(async (res, rej) => { decCount: 0, decSize: 0 } + }, + network: { + requests: 0, + totalTime: 0, + incomingBytes: 0, + outgoingBytes: 0 } }); } diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts index dc6a602e10..de0bde086b 100644 --- a/src/server/api/endpoints/drive/files.ts +++ b/src/server/api/endpoints/drive/files.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; -import DriveFile, { pack } from '../../../../models/drive-file'; +import DriveFile, { packMany } from '../../../../models/drive-file'; import { ILocalUser } from '../../../../models/user'; export const meta = { @@ -73,6 +73,5 @@ export default async (params: any, user: ILocalUser) => { }); // Serialize - const _files = await Promise.all(files.map(file => pack(file))); - return _files; + return await packMany(files); }; diff --git a/src/server/api/endpoints/drive/files/check_existence.ts b/src/server/api/endpoints/drive/files/check_existence.ts new file mode 100644 index 0000000000..73d75b7caf --- /dev/null +++ b/src/server/api/endpoints/drive/files/check_existence.ts @@ -0,0 +1,38 @@ +import $ from 'cafy'; +import DriveFile, { pack } from '../../../../../models/drive-file'; +import { ILocalUser } from '../../../../../models/user'; + +export const meta = { + desc: { + 'ja-JP': '与えられたMD5ハッシュ値を持つファイルがドライブに存在するかどうかを返します。', + 'en-US': 'Returns whether the file with the given MD5 hash exists in the user\'s drive.' + }, + + requireCredential: true, + + kind: 'drive-read', + + params: { + md5: $.str.note({ + desc: { + 'ja-JP': 'ファイルのMD5ハッシュ' + } + }) + } +}; + +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [md5, md5Err] = $.str.get(params.md5); + if (md5Err) return rej('invalid md5 param'); + + const file = await DriveFile.findOne({ + md5: md5, + 'metadata.userId': user._id + }); + + if (file === null) { + res({ file: null }); + } else { + res({ file: await pack(file) }); + } +}); diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index dfbd11d0c2..4b5ffa90e0 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -31,8 +31,8 @@ export const meta = { } }), - isSensitive: $.bool.optional.note({ - default: false, + isSensitive: $.bool.optional.nullable.note({ + default: null, desc: { 'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか', 'en-US': 'Whether this media is NSFW' diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts index ba9abfec61..3c7932c341 100644 --- a/src/server/api/endpoints/drive/files/update.ts +++ b/src/server/api/endpoints/drive/files/update.ts @@ -4,6 +4,7 @@ import DriveFile, { validateFileName, pack } from '../../../../../models/drive-f import { publishDriveStream } from '../../../../../stream'; import { ILocalUser } from '../../../../../models/user'; import getParams from '../../../get-params'; +import Note from '../../../../../models/note'; export const meta = { desc: { @@ -93,6 +94,18 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = } }); + // ドライブのファイルが非正規化されているドキュメントも更新 + Note.find({ + '_files._id': file._id + }).then(notes => { + notes.forEach(note => { + note._files[note._files.findIndex(f => f._id.equals(file._id))] = file; + Note.findOneAndUpdate({ _id: note._id }, { + _files: note._files + }); + }); + }); + // Serialize const fileObj = await pack(file); diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts index a9f3f7e9a5..3ac7dd0234 100644 --- a/src/server/api/endpoints/drive/stream.ts +++ b/src/server/api/endpoints/drive/stream.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; -import DriveFile, { pack } from '../../../../models/drive-file'; +import DriveFile, { packMany } from '../../../../models/drive-file'; import { ILocalUser } from '../../../../models/user'; export const meta = { @@ -63,5 +63,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = }); // Serialize - res(await Promise.all(files.map(file => pack(file)))); + res(await packMany(files)); }); diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index c9bea0e3d2..00aa904f08 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -57,7 +57,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = } // Create following - create(follower, followee); + await create(follower, followee); // Send response res(await pack(followee._id, user)); diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts index f3b4a73ae8..cdfbf43cd1 100644 --- a/src/server/api/endpoints/following/delete.ts +++ b/src/server/api/endpoints/following/delete.ts @@ -57,7 +57,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = } // Delete following - deleteFollowing(follower, followee); + await deleteFollowing(follower, followee); // Send response res(await pack(followee._id, user)); diff --git a/src/server/api/endpoints/games/reversi/match.ts b/src/server/api/endpoints/games/reversi/match.ts index aba400af1d..d7483a0bfd 100644 --- a/src/server/api/endpoints/games/reversi/match.ts +++ b/src/server/api/endpoints/games/reversi/match.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import ID from '../../../../../misc/cafy-id'; import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching'; import ReversiGame, { pack as packGame } from '../../../../../models/games/reversi/game'; import User, { ILocalUser } from '../../../../../models/user'; -import { publishUserStream, publishReversiStream } from '../../../../../stream'; +import { publishMainStream, publishReversiStream } from '../../../../../stream'; import { eighteight } from '../../../../../games/reversi/maps'; export const meta = { @@ -58,7 +58,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = }); if (other == 0) { - publishUserStream(user._id, 'reversi_no_invites'); + publishMainStream(user._id, 'reversi_no_invites'); } } else { // Fetch child @@ -94,6 +94,6 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = // 招待 publishReversiStream(child._id, 'invited', packed); - publishUserStream(child._id, 'reversi_invited', packed); + publishMainStream(child._id, 'reversiInvited', packed); } }); diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts index 01dfccc71c..0ec6a4ffec 100644 --- a/src/server/api/endpoints/hashtags/trend.ts +++ b/src/server/api/endpoints/hashtags/trend.ts @@ -1,4 +1,6 @@ import Note from '../../../../models/note'; +import { erase } from '../../../../prelude/array'; +import Meta from '../../../../models/meta'; /* トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 @@ -16,6 +18,9 @@ const max = 5; * Get trends of hashtags */ export default () => new Promise(async (res, rej) => { + const meta = await Meta.findOne({}); + const hidedTags = meta ? (meta.hidedTags || []).map(t => t.toLowerCase()) : []; + //#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計 const data = await Note.aggregate([{ $match: { @@ -52,6 +57,9 @@ export default () => new Promise(async (res, rej) => { // カウント data.map(x => x._id).forEach(x => { + // ブラックリストに登録されているタグなら弾く + if (hidedTags.includes(x.tag)) return; + const i = tags.findIndex(tag => tag.name == x.tag); if (i != -1) { tags[i].count++; @@ -85,8 +93,7 @@ export default () => new Promise(async (res, rej) => { //#endregion // タグを人気順に並べ替え - let hots = (await Promise.all(hotsPromises)) - .filter(x => x != null) + let hots = erase(null, await Promise.all(hotsPromises)) .sort((a, b) => b.count - a.count) .map(tag => tag.name) .slice(0, max); diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts index 1f99ef2d8d..5aa2070650 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -22,6 +22,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async ( // Serialize res(await pack(user, user, { detail: true, + includeHasUnreadNotes: true, includeSecrets: isSecure })); diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts index 32c1a55fb0..e7cf8a71a7 100644 --- a/src/server/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; -import Favorite, { pack } from '../../../../models/favorite'; +import Favorite, { packMany } from '../../../../models/favorite'; import { ILocalUser } from '../../../../models/user'; export const meta = { @@ -55,5 +55,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = .find(query, { limit, sort }); // Serialize - res(await Promise.all(favorites.map(favorite => pack(favorite, user)))); + res(await packMany(favorites, user)); }); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index 46242b9d9f..5cc836e362 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import Notification from '../../../../models/notification'; import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/notification'; +import { packMany } from '../../../../models/notification'; import { getFriendIds } from '../../common/get-friends'; import read from '../../common/read-notification'; import { ILocalUser } from '../../../../models/user'; @@ -83,7 +83,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = }); // Serialize - res(await Promise.all(notifications.map(notification => pack(notification)))); + res(await packMany(notifications)); // Mark all as read if (notifications.length > 0 && markAsRead) { diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts index ae03a86336..bf729ca091 100644 --- a/src/server/api/endpoints/i/pin.ts +++ b/src/server/api/endpoints/i/pin.ts @@ -1,31 +1,37 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; -import User, { ILocalUser } from '../../../../models/user'; -import Note from '../../../../models/note'; +import { ILocalUser } from '../../../../models/user'; import { pack } from '../../../../models/user'; +import { addPinned } from '../../../../services/i/pin'; +import getParams from '../../get-params'; -/** - * Pin note - */ -export default async (params: any, user: ILocalUser) => new Promise(async (res, rej) => { - // Get 'noteId' parameter - const [noteId, noteIdErr] = $.type(ID).get(params.noteId); - if (noteIdErr) return rej('invalid noteId param'); +export const meta = { + desc: { + 'ja-JP': '指定した投稿をピン留めします。' + }, - // Fetch pinee - const note = await Note.findOne({ - _id: noteId, - userId: user._id - }); + requireCredential: true, - if (note === null) { - return rej('note not found'); + kind: 'account-write', + + params: { + noteId: $.type(ID).note({ + desc: { + 'ja-JP': '対象の投稿のID' + } + }) } +}; - await User.update(user._id, { - $set: { - pinnedNoteId: note._id - } - }); +export default async (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + // Processing + try { + await addPinned(user, ps.noteId); + } catch (e) { + return rej(e.message); + } // Serialize const iObj = await pack(user, user, { diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts index fe4a5cd118..2d85f06cfa 100644 --- a/src/server/api/endpoints/i/regenerate_token.ts +++ b/src/server/api/endpoints/i/regenerate_token.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; import User, { ILocalUser } from '../../../../models/user'; -import { publishUserStream } from '../../../../stream'; +import { publishMainStream } from '../../../../stream'; import generateUserToken from '../../common/generate-native-user-token'; export const meta = { @@ -33,5 +33,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, res(); // Publish event - publishUserStream(user._id, 'my_token_regenerated'); + publishMainStream(user._id, 'myTokenRegenerated'); }); diff --git a/src/server/api/endpoints/i/unpin.ts b/src/server/api/endpoints/i/unpin.ts new file mode 100644 index 0000000000..2a81993e4b --- /dev/null +++ b/src/server/api/endpoints/i/unpin.ts @@ -0,0 +1,43 @@ +import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; +import { ILocalUser } from '../../../../models/user'; +import { pack } from '../../../../models/user'; +import { removePinned } from '../../../../services/i/pin'; +import getParams from '../../get-params'; + +export const meta = { + desc: { + 'ja-JP': '指定した投稿のピン留めを解除します。' + }, + + requireCredential: true, + + kind: 'account-write', + + params: { + noteId: $.type(ID).note({ + desc: { + 'ja-JP': '対象の投稿のID' + } + }) + } +}; + +export default async (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + // Processing + try { + await removePinned(user, ps.noteId); + } catch (e) { + return rej(e.message); + } + + // Serialize + const iObj = await pack(user, user, { + detail: true + }); + + // Send response + res(iObj); +}); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index cdb4eb3f56..548ce5cadb 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -1,10 +1,12 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack, ILocalUser } from '../../../../models/user'; -import { publishUserStream } from '../../../../stream'; +import { publishMainStream } from '../../../../stream'; import DriveFile from '../../../../models/drive-file'; import acceptAllFollowRequests from '../../../../services/following/requests/accept-all'; import { IApp } from '../../../../models/app'; import config from '../../../../config'; +import { publishToFollowers } from '../../../../services/i/update'; +import getParams from '../../get-params'; export const meta = { desc: { @@ -14,75 +16,111 @@ export const meta = { requireCredential: true, - kind: 'account-write' -}; + kind: 'account-write', -export default async (params: any, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => { - const isSecure = user != null && app == null; + params: { + name: $.str.optional.nullable.pipe(isValidName).note({ + desc: { + 'ja-JP': '名前(ハンドルネームやニックネーム)' + } + }), - const updates = {} as any; + description: $.str.optional.nullable.pipe(isValidDescription).note({ + desc: { + 'ja-JP': 'アカウントの説明や自己紹介' + } + }), - // Get 'name' parameter - const [name, nameErr] = $.str.optional.nullable.pipe(isValidName).get(params.name); - if (nameErr) return rej('invalid name param'); - if (name) updates.name = name; + location: $.str.optional.nullable.pipe(isValidLocation).note({ + desc: { + 'ja-JP': '住んでいる地域、所在' + } + }), - // Get 'description' parameter - const [description, descriptionErr] = $.str.optional.nullable.pipe(isValidDescription).get(params.description); - if (descriptionErr) return rej('invalid description param'); - if (description !== undefined) updates.description = description; + birthday: $.str.optional.nullable.pipe(isValidBirthday).note({ + desc: { + 'ja-JP': '誕生日 (YYYY-MM-DD形式)' + } + }), - // Get 'location' parameter - const [location, locationErr] = $.str.optional.nullable.pipe(isValidLocation).get(params.location); - if (locationErr) return rej('invalid location param'); - if (location !== undefined) updates['profile.location'] = location; + avatarId: $.type(ID).optional.nullable.note({ + desc: { + 'ja-JP': 'アイコンに設定する画像のドライブファイルID' + } + }), - // Get 'birthday' parameter - const [birthday, birthdayErr] = $.str.optional.nullable.pipe(isValidBirthday).get(params.birthday); - if (birthdayErr) return rej('invalid birthday param'); - if (birthday !== undefined) updates['profile.birthday'] = birthday; + bannerId: $.type(ID).optional.nullable.note({ + desc: { + 'ja-JP': 'バナーに設定する画像のドライブファイルID' + } + }), - // Get 'avatarId' parameter - const [avatarId, avatarIdErr] = $.type(ID).optional.nullable.get(params.avatarId); - if (avatarIdErr) return rej('invalid avatarId param'); - if (avatarId !== undefined) updates.avatarId = avatarId; + wallpaperId: $.type(ID).optional.nullable.note({ + desc: { + 'ja-JP': '壁紙に設定する画像のドライブファイルID' + } + }), - // Get 'bannerId' parameter - const [bannerId, bannerIdErr] = $.type(ID).optional.nullable.get(params.bannerId); - if (bannerIdErr) return rej('invalid bannerId param'); - if (bannerId !== undefined) updates.bannerId = bannerId; + isLocked: $.bool.optional.note({ + desc: { + 'ja-JP': '鍵アカウントか否か' + } + }), - // Get 'wallpaperId' parameter - const [wallpaperId, wallpaperIdErr] = $.type(ID).optional.nullable.get(params.wallpaperId); - if (wallpaperIdErr) return rej('invalid wallpaperId param'); - if (wallpaperId !== undefined) updates.wallpaperId = wallpaperId; + isBot: $.bool.optional.note({ + desc: { + 'ja-JP': 'Botか否か' + } + }), - // Get 'isLocked' parameter - const [isLocked, isLockedErr] = $.bool.optional.get(params.isLocked); - if (isLockedErr) return rej('invalid isLocked param'); - if (isLocked != null) updates.isLocked = isLocked; + isCat: $.bool.optional.note({ + desc: { + 'ja-JP': '猫か否か' + } + }), + + autoWatch: $.bool.optional.note({ + desc: { + 'ja-JP': '投稿の自動ウォッチをするか否か' + } + }), + + alwaysMarkNsfw: $.bool.optional.note({ + desc: { + 'ja-JP': 'アップロードするメディアをデフォルトで「閲覧注意」として設定するか' + } + }), + } +}; + +export default async (params: any, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; - // Get 'isBot' parameter - const [isBot, isBotErr] = $.bool.optional.get(params.isBot); - if (isBotErr) return rej('invalid isBot param'); - if (isBot != null) updates.isBot = isBot; + const isSecure = user != null && app == null; - // Get 'isCat' parameter - const [isCat, isCatErr] = $.bool.optional.get(params.isCat); - if (isCatErr) return rej('invalid isCat param'); - if (isCat != null) updates.isCat = isCat; + const updates = {} as any; - // Get 'autoWatch' parameter - const [autoWatch, autoWatchErr] = $.bool.optional.get(params.autoWatch); - if (autoWatchErr) return rej('invalid autoWatch param'); - if (autoWatch != null) updates['settings.autoWatch'] = autoWatch; + if (ps.name !== undefined) updates.name = ps.name; + if (ps.description !== undefined) updates.description = ps.description; + if (ps.location !== undefined) updates['profile.location'] = ps.location; + if (ps.birthday !== undefined) updates['profile.birthday'] = ps.birthday; + if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; + if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; + if (ps.wallpaperId !== undefined) updates.wallpaperId = ps.wallpaperId; + if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked; + if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot; + if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat; + if (typeof ps.autoWatch == 'boolean') updates['settings.autoWatch'] = ps.autoWatch; + if (typeof ps.alwaysMarkNsfw == 'boolean') updates['settings.alwaysMarkNsfw'] = ps.alwaysMarkNsfw; - if (avatarId) { + if (ps.avatarId) { const avatar = await DriveFile.findOne({ - _id: avatarId + _id: ps.avatarId }); if (avatar == null) return rej('avatar not found'); + if (!avatar.contentType.startsWith('image/')) return rej('avatar not an image'); updates.avatarUrl = avatar.metadata.thumbnailUrl || avatar.metadata.url || `${config.drive_url}/${avatar._id}`; @@ -91,12 +129,13 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a } } - if (bannerId) { + if (ps.bannerId) { const banner = await DriveFile.findOne({ - _id: bannerId + _id: ps.bannerId }); if (banner == null) return rej('banner not found'); + if (!banner.contentType.startsWith('image/')) return rej('banner not an image'); updates.bannerUrl = banner.metadata.url || `${config.drive_url}/${banner._id}`; @@ -105,13 +144,13 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a } } - if (wallpaperId !== undefined) { - if (wallpaperId === null) { + if (ps.wallpaperId !== undefined) { + if (ps.wallpaperId === null) { updates.wallpaperUrl = null; updates.wallpaperColor = null; } else { const wallpaper = await DriveFile.findOne({ - _id: wallpaperId + _id: ps.wallpaperId }); if (wallpaper == null) return rej('wallpaper not found'); @@ -138,10 +177,13 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a res(iObj); // Publish meUpdated event - publishUserStream(user._id, 'meUpdated', iObj); + publishMainStream(user._id, 'meUpdated', iObj); // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 - if (user.isLocked && isLocked === false) { + if (user.isLocked && ps.isLocked === false) { acceptAllFollowRequests(user); } + + // フォロワーにUpdateを配信 + publishToFollowers(user._id); }); diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts index aed93c792f..2c05299dff 100644 --- a/src/server/api/endpoints/i/update_client_setting.ts +++ b/src/server/api/endpoints/i/update_client_setting.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import User, { ILocalUser } from '../../../../models/user'; -import { publishUserStream } from '../../../../stream'; +import { publishMainStream } from '../../../../stream'; export const meta = { requireCredential: true, @@ -26,7 +26,7 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, res(); // Publish event - publishUserStream(user._id, 'clientSettingUpdated', { + publishMainStream(user._id, 'clientSettingUpdated', { key: name, value }); diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts index ffca9b90b3..27afc9fe5a 100644 --- a/src/server/api/endpoints/i/update_home.ts +++ b/src/server/api/endpoints/i/update_home.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import User, { ILocalUser } from '../../../../models/user'; -import { publishUserStream } from '../../../../stream'; +import { publishMainStream } from '../../../../stream'; export const meta = { requireCredential: true, @@ -25,5 +25,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, res(); - publishUserStream(user._id, 'home_updated', home); + publishMainStream(user._id, 'homeUpdated', home); }); diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts index 0b72fbe2c1..1d4df389e4 100644 --- a/src/server/api/endpoints/i/update_mobile_home.ts +++ b/src/server/api/endpoints/i/update_mobile_home.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import User, { ILocalUser } from '../../../../models/user'; -import { publishUserStream } from '../../../../stream'; +import { publishMainStream } from '../../../../stream'; export const meta = { requireCredential: true, @@ -24,5 +24,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, res(); - publishUserStream(user._id, 'mobile_home_updated', home); + publishMainStream(user._id, 'mobileHomeUpdated', home); }); diff --git a/src/server/api/endpoints/i/update_widget.ts b/src/server/api/endpoints/i/update_widget.ts index 5cbe7c07a3..92499493eb 100644 --- a/src/server/api/endpoints/i/update_widget.ts +++ b/src/server/api/endpoints/i/update_widget.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import User, { ILocalUser } from '../../../../models/user'; -import { publishUserStream } from '../../../../stream'; +import { publishMainStream } from '../../../../stream'; export const meta = { requireCredential: true, @@ -73,7 +73,7 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, //#endregion if (widget) { - publishUserStream(user._id, 'widgetUpdated', { + publishMainStream(user._id, 'widgetUpdated', { id, data }); diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index a6fabcfa45..cb115cf987 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -6,7 +6,7 @@ import User, { ILocalUser } from '../../../../../models/user'; import Mute from '../../../../../models/mute'; import DriveFile from '../../../../../models/drive-file'; import { pack } from '../../../../../models/messaging-message'; -import { publishUserStream } from '../../../../../stream'; +import { publishMainStream } from '../../../../../stream'; import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../stream'; import pushSw from '../../../../../push-sw'; @@ -74,7 +74,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = createdAt: new Date(), fileId: file ? file._id : undefined, recipientId: recipient._id, - text: text ? text : undefined, + text: text ? text.trim() : undefined, userId: user._id, isRead: false }); @@ -88,12 +88,12 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = // 自分のストリーム publishMessagingStream(message.userId, message.recipientId, 'message', messageObj); publishMessagingIndexStream(message.userId, 'message', messageObj); - publishUserStream(message.userId, 'messaging_message', messageObj); + publishMainStream(message.userId, 'messagingMessage', messageObj); // 相手のストリーム publishMessagingStream(message.recipientId, message.userId, 'message', messageObj); publishMessagingIndexStream(message.recipientId, 'message', messageObj); - publishUserStream(message.recipientId, 'messaging_message', messageObj); + publishMainStream(message.recipientId, 'messagingMessage', messageObj); // Update flag User.update({ _id: recipient._id }, { @@ -102,7 +102,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = } }); - // 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する + // 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する setTimeout(async () => { const freshMessage = await Message.findOne({ _id: message._id }, { isRead: true }); if (!freshMessage.isRead) { @@ -117,10 +117,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = } //#endregion - publishUserStream(message.recipientId, 'unread_messaging_message', messageObj); - pushSw(message.recipientId, 'unread_messaging_message', messageObj); + publishMainStream(message.recipientId, 'unreadMessagingMessage', messageObj); + pushSw(message.recipientId, 'unreadMessagingMessage', messageObj); } - }, 3000); + }, 2000); // 履歴作成(自分) History.update({ diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 2b39f26b8e..c76d7f2e8f 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -4,6 +4,7 @@ import * as os from 'os'; import config from '../../../config'; import Meta from '../../../models/meta'; +import { ILocalUser } from '../../../models/user'; const pkg = require('../../../../package.json'); const client = require('../../../../built/client/meta.json'); @@ -11,7 +12,7 @@ const client = require('../../../../built/client/meta.json'); /** * Show core info */ -export default () => new Promise(async (res, rej) => { +export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { const meta: any = (await Meta.findOne()) || {}; res({ @@ -31,9 +32,22 @@ export default () => new Promise(async (res, rej) => { model: os.cpus()[0].model, cores: os.cpus().length }, - broadcasts: meta.broadcasts, + broadcasts: meta.broadcasts || [], disableRegistration: meta.disableRegistration, + disableLocalTimeline: meta.disableLocalTimeline, + driveCapacityPerLocalUserMb: config.localDriveCapacityMb, recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null, - swPublickey: config.sw ? config.sw.public_key : null + swPublickey: config.sw ? config.sw.public_key : null, + hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined, + bannerUrl: meta.bannerUrl, + features: { + registration: !meta.disableRegistration, + localTimeLine: !meta.disableLocalTimeline, + elasticsearch: config.elasticsearch ? true : false, + recaptcha: config.recaptcha ? true : false, + objectStorage: config.drive && config.drive.storage === 'minio', + twitter: config.twitter ? true : false, + serviceWorker: config.sw ? true : false + } }); }); diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts index 029bc1a95e..d65710d33f 100644 --- a/src/server/api/endpoints/notes.ts +++ b/src/server/api/endpoints/notes.ts @@ -1,51 +1,65 @@ -/** - * Module dependencies - */ import $ from 'cafy'; import ID from '../../../misc/cafy-id'; -import Note, { pack } from '../../../models/note'; +import Note, { packMany } from '../../../models/note'; +import getParams from '../get-params'; -/** - * Get all notes - */ -export default (params: any) => new Promise(async (res, rej) => { - // Get 'local' parameter - const [local, localErr] = $.bool.optional.get(params.local); - if (localErr) return rej('invalid local param'); +export const meta = { + desc: { + 'ja-JP': '投稿を取得します。' + }, + + params: { + local: $.bool.optional.note({ + desc: { + 'ja-JP': 'ローカルの投稿に限定するか否か' + } + }), + + reply: $.bool.optional.note({ + desc: { + 'ja-JP': '返信に限定するか否か' + } + }), - // Get 'reply' parameter - const [reply, replyErr] = $.bool.optional.get(params.reply); - if (replyErr) return rej('invalid reply param'); + renote: $.bool.optional.note({ + desc: { + 'ja-JP': 'Renoteに限定するか否か' + } + }), - // Get 'renote' parameter - const [renote, renoteErr] = $.bool.optional.get(params.renote); - if (renoteErr) return rej('invalid renote param'); + withFiles: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か' + } + }), - // Get 'media' parameter - const [media, mediaErr] = $.bool.optional.get(params.media); - if (mediaErr) return rej('invalid media param'); + media: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + } + }), - // Get 'poll' parameter - const [poll, pollErr] = $.bool.optional.get(params.poll); - if (pollErr) return rej('invalid poll param'); + poll: $.bool.optional.note({ + desc: { + 'ja-JP': 'アンケートが添付された投稿に限定するか否か' + } + }), - // Get 'bot' parameter - //const [bot, botErr] = $.bool.optional.get(params.bot); - //if (botErr) return rej('invalid bot param'); + limit: $.num.optional.range(1, 100).note({ + default: 10 + }), - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) return rej('invalid limit param'); + sinceId: $.type(ID).optional.note({}), - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); - if (sinceIdErr) return rej('invalid sinceId param'); + untilId: $.type(ID).optional.note({}), + } +}; - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); - if (untilIdErr) return rej('invalid untilId param'); +export default (params: any) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; // Check if both of sinceId and untilId is specified - if (sinceId && untilId) { + if (ps.sinceId && ps.untilId) { return rej('cannot set sinceId and untilId'); } @@ -56,35 +70,37 @@ export default (params: any) => new Promise(async (res, rej) => { const query = { visibility: 'public' } as any; - if (sinceId) { + if (ps.sinceId) { sort._id = 1; query._id = { - $gt: sinceId + $gt: ps.sinceId }; - } else if (untilId) { + } else if (ps.untilId) { query._id = { - $lt: untilId + $lt: ps.untilId }; } - if (local) { + if (ps.local) { query['_user.host'] = null; } - if (reply != undefined) { - query.replyId = reply ? { $exists: true, $ne: null } : null; + if (ps.reply != undefined) { + query.replyId = ps.reply ? { $exists: true, $ne: null } : null; } - if (renote != undefined) { - query.renoteId = renote ? { $exists: true, $ne: null } : null; + if (ps.renote != undefined) { + query.renoteId = ps.renote ? { $exists: true, $ne: null } : null; } - if (media != undefined) { - query.mediaIds = media ? { $exists: true, $ne: null } : []; + const withFiles = ps.withFiles != undefined ? ps.withFiles : ps.media; + + if (withFiles) { + query.fileIds = withFiles ? { $exists: true, $ne: null } : []; } - if (poll != undefined) { - query.poll = poll ? { $exists: true, $ne: null } : null; + if (ps.poll != undefined) { + query.poll = ps.poll ? { $exists: true, $ne: null } : null; } // TODO @@ -95,10 +111,10 @@ export default (params: any) => new Promise(async (res, rej) => { // Issue query const notes = await Note .find(query, { - limit: limit, + limit: ps.limit, sort: sort }); // Serialize - res(await Promise.all(notes.map(note => pack(note)))); + res(await packMany(notes)); }); diff --git a/src/server/api/endpoints/notes/conversation.ts b/src/server/api/endpoints/notes/conversation.ts index 2782d14155..0c23f9e5fc 100644 --- a/src/server/api/endpoints/notes/conversation.ts +++ b/src/server/api/endpoints/notes/conversation.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; -import Note, { pack, INote } from '../../../../models/note'; +import Note, { packMany, INote } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; /** @@ -52,5 +52,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = } // Serialize - res(await Promise.all(conversation.map(note => pack(note, user)))); + res(await packMany(conversation, user)); }); diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 04f5f7562e..96745132a3 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -71,9 +71,15 @@ export const meta = { ref: 'geo' }), + fileIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({ + desc: { + 'ja-JP': '添付するファイル' + } + }), + mediaIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({ desc: { - 'ja-JP': '添付するメディア' + 'ja-JP': '添付するファイル (このパラメータは廃止予定です。代わりに fileIds を使ってください。)' } }), @@ -124,26 +130,16 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async ( } let files: IDriveFile[] = []; - if (ps.mediaIds !== undefined) { - // Fetch files - // forEach だと途中でエラーなどがあっても return できないので - // 敢えて for を使っています。 - for (const mediaId of ps.mediaIds) { - // Fetch file - // SELECT _id - const entity = await DriveFile.findOne({ - _id: mediaId, + const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; + if (fileIds != null) { + files = await Promise.all(fileIds.map(fileId => { + return DriveFile.findOne({ + _id: fileId, 'metadata.userId': user._id }); + })); - if (entity === null) { - return rej('file not found'); - } else { - files.push(entity); - } - } - } else { - files = null; + files = files.filter(file => file != null); } let renote: INote = null; @@ -155,7 +151,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async ( if (renote == null) { return rej('renoteee is not found'); - } else if (renote.renoteId && !renote.text && !renote.mediaIds) { + } else if (renote.renoteId && !renote.text && !renote.fileIds) { return rej('cannot renote to renote'); } } @@ -176,7 +172,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async ( } // 返信対象が引用でないRenoteだったらエラー - if (reply.renoteId && !reply.text && !reply.mediaIds) { + if (reply.renoteId && !reply.text && !reply.fileIds) { return rej('cannot reply to renote'); } } @@ -191,13 +187,13 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async ( // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー if ((ps.text === undefined || ps.text === null) && files === null && renote === null && ps.poll === undefined) { - return rej('text, mediaIds, renoteId or poll is required'); + return rej('text, fileIds, renoteId or poll is required'); } // 投稿を作成 const note = await create(user, { createdAt: new Date(), - media: files, + files: files, poll: ps.poll, text: ps.text, reply, diff --git a/src/server/api/endpoints/notes/delete.ts b/src/server/api/endpoints/notes/delete.ts index 6d9826cf7b..2fe36897c0 100644 --- a/src/server/api/endpoints/notes/delete.ts +++ b/src/server/api/endpoints/notes/delete.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import Note from '../../../../models/note'; import deleteNote from '../../../../services/note/delete'; -import { ILocalUser } from '../../../../models/user'; +import User, { ILocalUser } from '../../../../models/user'; export const meta = { desc: { @@ -21,15 +21,18 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = // Fetch note const note = await Note.findOne({ - _id: noteId, - userId: user._id + _id: noteId }); if (note === null) { return rej('note not found'); } - await deleteNote(user, note); + if (!user.isAdmin && !note.userId.equals(user._id)) { + return rej('access denied'); + } + + await deleteNote(await User.findOne({ _id: note.userId }), note); res(); }); diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts index daf7780abc..9aefb701ae 100644 --- a/src/server/api/endpoints/notes/favorites/create.ts +++ b/src/server/api/endpoints/notes/favorites/create.ts @@ -2,6 +2,7 @@ import $ from 'cafy'; import ID from '../../../../../misc/cafy-id'; import Favorite from '../../../../../models/favorite'; import Note from '../../../../../models/note'; import { ILocalUser } from '../../../../../models/user'; +import getParams from '../../../get-params'; export const meta = { desc: { @@ -11,17 +12,24 @@ export const meta = { requireCredential: true, - kind: 'favorite-write' + kind: 'favorite-write', + + params: { + noteId: $.type(ID).note({ + desc: { + 'ja-JP': '対象の投稿のID' + } + }) + } }; export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { - // Get 'noteId' parameter - const [noteId, noteIdErr] = $.type(ID).get(params.noteId); - if (noteIdErr) return rej('invalid noteId param'); + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); // Get favoritee const note = await Note.findOne({ - _id: noteId + _id: ps.noteId }); if (note === null) { diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts index 8f7233e308..8362143bb2 100644 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ b/src/server/api/endpoints/notes/global-timeline.ts @@ -1,42 +1,52 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; +import { packMany } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; +import getParams from '../../get-params'; +import { countIf } from '../../../../prelude/array'; -/** - * Get timeline of global - */ -export default async (params: any, user: ILocalUser) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) throw 'invalid limit param'; +export const meta = { + desc: { + 'ja-JP': 'グローバルタイムラインを取得します。' + }, + + params: { + withFiles: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か' + } + }), + + mediaOnly: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + } + }), + + limit: $.num.optional.range(1, 100).note({ + default: 10 + }), + + sinceId: $.type(ID).optional.note({}), - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); - if (sinceIdErr) throw 'invalid sinceId param'; + untilId: $.type(ID).optional.note({}), - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); - if (untilIdErr) throw 'invalid untilId param'; + sinceDate: $.num.optional.note({}), - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate); - if (sinceDateErr) throw 'invalid sinceDate param'; + untilDate: $.num.optional.note({}), + } +}; - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate); - if (untilDateErr) throw 'invalid untilDate param'; +export default async (params: any, user: ILocalUser) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } - // Get 'mediaOnly' parameter - const [mediaOnly, mediaOnlyErr] = $.bool.optional.get(params.mediaOnly); - if (mediaOnlyErr) throw 'invalid mediaOnly param'; - // ミュートしているユーザーを取得 const mutedUserIds = user ? (await Mute.find({ muterId: user._id @@ -68,27 +78,29 @@ export default async (params: any, user: ILocalUser) => { }; } - if (mediaOnly) { - query.mediaIds = { $exists: true, $ne: [] }; + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + + if (withFiles) { + query.fileIds = { $exists: true, $ne: [] }; } - if (sinceId) { + if (ps.sinceId) { sort._id = 1; query._id = { - $gt: sinceId + $gt: ps.sinceId }; - } else if (untilId) { + } else if (ps.untilId) { query._id = { - $lt: untilId + $lt: ps.untilId }; - } else if (sinceDate) { + } else if (ps.sinceDate) { sort._id = 1; query.createdAt = { - $gt: new Date(sinceDate) + $gt: new Date(ps.sinceDate) }; - } else if (untilDate) { + } else if (ps.untilDate) { query.createdAt = { - $lt: new Date(untilDate) + $lt: new Date(ps.untilDate) }; } //#endregion @@ -96,10 +108,10 @@ export default async (params: any, user: ILocalUser) => { // Issue query const timeline = await Note .find(query, { - limit: limit, + limit: ps.limit, sort: sort }); // Serialize - return await Promise.all(timeline.map(note => pack(note, user))); + return await packMany(timeline, user); }; diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index 2dbb1190c1..14b4432b33 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -2,13 +2,12 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; import { getFriends } from '../../common/get-friends'; -import { pack } from '../../../../models/note'; +import { packMany } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; import getParams from '../../get-params'; +import { countIf } from '../../../../prelude/array'; export const meta = { - name: 'notes/hybrid-timeline', - desc: { 'ja-JP': 'ハイブリッドタイムラインを取得します。' }, @@ -66,23 +65,26 @@ export const meta = { } }), + withFiles: $.bool.optional.note({ + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' + } + }), + mediaOnly: $.bool.optional.note({ desc: { - 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' } }), } }; -/** - * Get hybrid timeline of myself - */ export default async (params: any, user: ILocalUser) => { const [ps, psErr] = getParams(meta, params); if (psErr) throw psErr; // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { + if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } @@ -164,7 +166,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -180,7 +182,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -196,16 +198,16 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] }); } - if (ps.mediaOnly) { + if (ps.withFiles || ps.mediaOnly) { query.$and.push({ - mediaIds: { $exists: true, $ne: [] } + fileIds: { $exists: true, $ne: [] } }); } @@ -238,5 +240,5 @@ export default async (params: any, user: ILocalUser) => { }); // Serialize - return await Promise.all(timeline.map(note => pack(note, user))); + return await packMany(timeline, user); }; diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index bbcc6303ca..8ab07d8ea7 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -1,42 +1,65 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; +import { packMany } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; +import getParams from '../../get-params'; +import { countIf } from '../../../../prelude/array'; -/** - * Get timeline of local - */ -export default async (params: any, user: ILocalUser) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) throw 'invalid limit param'; +export const meta = { + desc: { + 'ja-JP': 'ローカルタイムラインを取得します。' + }, + + params: { + withFiles: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か' + } + }), + + mediaOnly: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + } + }), + + fileType: $.arr($.str).optional.note({ + desc: { + 'ja-JP': '指定された種類のファイルが添付された投稿のみを取得します' + } + }), + + excludeNsfw: $.bool.optional.note({ + default: false, + desc: { + 'ja-JP': 'true にすると、NSFW指定されたファイルを除外します(fileTypeが指定されている場合のみ有効)' + } + }), - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); - if (sinceIdErr) throw 'invalid sinceId param'; + limit: $.num.optional.range(1, 100).note({ + default: 10 + }), - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); - if (untilIdErr) throw 'invalid untilId param'; + sinceId: $.type(ID).optional.note({}), - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate); - if (sinceDateErr) throw 'invalid sinceDate param'; + untilId: $.type(ID).optional.note({}), - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate); - if (untilDateErr) throw 'invalid untilDate param'; + sinceDate: $.num.optional.note({}), + + untilDate: $.num.optional.note({}), + } +}; + +export default async (params: any, user: ILocalUser) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } - // Get 'mediaOnly' parameter - const [mediaOnly, mediaOnlyErr] = $.bool.optional.get(params.mediaOnly); - if (mediaOnlyErr) throw 'invalid mediaOnly param'; - // ミュートしているユーザーを取得 const mutedUserIds = user ? (await Mute.find({ muterId: user._id @@ -69,27 +92,43 @@ export default async (params: any, user: ILocalUser) => { }; } - if (mediaOnly) { - query.mediaIds = { $exists: true, $ne: [] }; + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + + if (withFiles) { + query.fileIds = { $exists: true, $ne: [] }; + } + + if (ps.fileType) { + query.fileIds = { $exists: true, $ne: [] }; + + query['_files.contentType'] = { + $in: ps.fileType + }; + + if (ps.excludeNsfw) { + query['_files.metadata.isSensitive'] = { + $ne: true + }; + } } - if (sinceId) { + if (ps.sinceId) { sort._id = 1; query._id = { - $gt: sinceId + $gt: ps.sinceId }; - } else if (untilId) { + } else if (ps.untilId) { query._id = { - $lt: untilId + $lt: ps.untilId }; - } else if (sinceDate) { + } else if (ps.sinceDate) { sort._id = 1; query.createdAt = { - $gt: new Date(sinceDate) + $gt: new Date(ps.sinceDate) }; - } else if (untilDate) { + } else if (ps.untilDate) { query.createdAt = { - $lt: new Date(untilDate) + $lt: new Date(ps.untilDate) }; } //#endregion @@ -97,10 +136,10 @@ export default async (params: any, user: ILocalUser) => { // Issue query const timeline = await Note .find(query, { - limit: limit, + limit: ps.limit, sort: sort }); // Serialize - return await Promise.all(timeline.map(note => pack(note, user))); + return await packMany(timeline, user); }; diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index a7fb14d8a9..592a94263d 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -1,8 +1,10 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import Note from '../../../../models/note'; import { getFriendIds } from '../../common/get-friends'; -import { pack } from '../../../../models/note'; +import { packMany } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; +import getParams from '../../get-params'; +import read from '../../../../services/note/read'; export const meta = { desc: { @@ -10,42 +12,55 @@ export const meta = { 'en-US': 'Get mentions of myself.' }, - requireCredential: true -}; + requireCredential: true, -export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { - // Get 'following' parameter - const [following = false, followingError] = - $.bool.optional.get(params.following); - if (followingError) return rej('invalid following param'); + params: { + following: $.bool.optional.note({ + default: false + }), + + limit: $.num.optional.range(1, 100).note({ + default: 10 + }), - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) return rej('invalid limit param'); + sinceId: $.type(ID).optional.note({ + }), - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); - if (sinceIdErr) return rej('invalid sinceId param'); + untilId: $.type(ID).optional.note({ + }), + + visibility: $.str.optional.note({ + }), + } +}; - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); - if (untilIdErr) return rej('invalid untilId param'); +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; // Check if both of sinceId and untilId is specified - if (sinceId && untilId) { + if (ps.sinceId && ps.untilId) { return rej('cannot set sinceId and untilId'); } // Construct query const query = { - mentions: user._id + $or: [{ + mentions: user._id + }, { + visibleUserIds: user._id + }] } as any; const sort = { _id: -1 }; - if (following) { + if (ps.visibility) { + query.visibility = ps.visibility; + } + + if (ps.following) { const followingIds = await getFriendIds(user._id); query.userId = { @@ -53,26 +68,26 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = }; } - if (sinceId) { + if (ps.sinceId) { sort._id = 1; query._id = { - $gt: sinceId + $gt: ps.sinceId }; - } else if (untilId) { + } else if (ps.untilId) { query._id = { - $lt: untilId + $lt: ps.untilId }; } // Issue query const mentions = await Note .find(query, { - limit: limit, + limit: ps.limit, sort: sort }); + mentions.forEach(note => read(user._id, note._id)); + // Serialize - res(await Promise.all(mentions.map(async mention => - await pack(mention, user) - ))); + res(await packMany(mentions, user)); }); diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts index ab80e7f5d0..3b78d62fd3 100644 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -72,7 +72,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = $inc: inc }); - publishNoteStream(note._id, 'poll_voted'); + publishNoteStream(note._id, 'pollVoted', { + choice: choice, + userId: user._id.toHexString() + }); // Notify notify(note.userId, user._id, 'poll_vote', { diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts index 0781db16c5..ec68f065d8 100644 --- a/src/server/api/endpoints/notes/reactions/create.ts +++ b/src/server/api/endpoints/notes/reactions/create.ts @@ -43,6 +43,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = return rej('note not found'); } + if (note.deletedAt != null) { + return rej('this not is already deleted'); + } + try { await create(user, note, ps.reaction); } catch (e) { diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts index 44c80afc4a..b2f8f94f69 100644 --- a/src/server/api/endpoints/notes/replies.ts +++ b/src/server/api/endpoints/notes/replies.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; -import Note, { pack } from '../../../../models/note'; +import Note, { packMany } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; /** @@ -30,5 +30,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = const ids = (note._replyIds || []).slice(offset, offset + limit); // Serialize - res(await Promise.all(ids.map(id => pack(id, user)))); + res(await packMany(ids, user)); }); diff --git a/src/server/api/endpoints/notes/reposts.ts b/src/server/api/endpoints/notes/reposts.ts index 05e68302ba..2c6e1a499f 100644 --- a/src/server/api/endpoints/notes/reposts.ts +++ b/src/server/api/endpoints/notes/reposts.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; -import Note, { pack } from '../../../../models/note'; +import Note, { packMany } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; /** @@ -62,6 +62,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = }); // Serialize - res(await Promise.all(renotes.map(async note => - await pack(note, user)))); + res(await packMany(renotes, user)); }); diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts index 9124899ad8..2755a70483 100644 --- a/src/server/api/endpoints/notes/search.ts +++ b/src/server/api/endpoints/notes/search.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import * as mongo from 'mongodb'; import Note from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; -import { pack } from '../../../../models/note'; +import { packMany } from '../../../../models/note'; import es from '../../../../db/elasticsearch'; export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { @@ -60,6 +60,6 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } }); - res(await Promise.all(notes.map(note => pack(note, me)))); + res(await packMany(notes, me)); }); }); diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts index e092275fe8..d380f27f9c 100644 --- a/src/server/api/endpoints/notes/search_by_tag.ts +++ b/src/server/api/endpoints/notes/search_by_tag.ts @@ -3,120 +3,171 @@ import Note from '../../../../models/note'; import User, { ILocalUser } from '../../../../models/user'; import Mute from '../../../../models/mute'; import { getFriendIds } from '../../common/get-friends'; -import { pack } from '../../../../models/note'; +import { packMany } from '../../../../models/note'; +import getParams from '../../get-params'; +import { erase } from '../../../../prelude/array'; -/** - * Search notes by tag - */ -export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { - // Get 'tag' parameter - const [tag, tagError] = $.str.get(params.tag); - if (tagError) return rej('invalid tag param'); +export const meta = { + desc: { + 'ja-JP': '指定されたタグが付けられた投稿を取得します。' + }, - // Get 'includeUserIds' parameter - const [includeUserIds = [], includeUserIdsErr] = $.arr($.type(ID)).optional.get(params.includeUserIds); - if (includeUserIdsErr) return rej('invalid includeUserIds param'); + params: { + tag: $.str.optional.note({ + desc: { + 'ja-JP': 'タグ' + } + }), - // Get 'excludeUserIds' parameter - const [excludeUserIds = [], excludeUserIdsErr] = $.arr($.type(ID)).optional.get(params.excludeUserIds); - if (excludeUserIdsErr) return rej('invalid excludeUserIds param'); + query: $.arr($.arr($.str)).optional.note({ + desc: { + 'ja-JP': 'クエリ' + } + }), - // Get 'includeUserUsernames' parameter - const [includeUserUsernames = [], includeUserUsernamesErr] = $.arr($.str).optional.get(params.includeUserUsernames); - if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param'); + includeUserIds: $.arr($.type(ID)).optional.note({ + default: [] + }), - // Get 'excludeUserUsernames' parameter - const [excludeUserUsernames = [], excludeUserUsernamesErr] = $.arr($.str).optional.get(params.excludeUserUsernames); - if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param'); + excludeUserIds: $.arr($.type(ID)).optional.note({ + default: [] + }), - // Get 'following' parameter - const [following = null, followingErr] = $.bool.optional.nullable.get(params.following); - if (followingErr) return rej('invalid following param'); + includeUserUsernames: $.arr($.str).optional.note({ + default: [] + }), - // Get 'mute' parameter - const [mute = 'mute_all', muteErr] = $.str.optional.get(params.mute); - if (muteErr) return rej('invalid mute param'); + excludeUserUsernames: $.arr($.str).optional.note({ + default: [] + }), - // Get 'reply' parameter - const [reply = null, replyErr] = $.bool.optional.nullable.get(params.reply); - if (replyErr) return rej('invalid reply param'); + following: $.bool.optional.nullable.note({ + default: null + }), - // Get 'renote' parameter - const [renote = null, renoteErr] = $.bool.optional.nullable.get(params.renote); - if (renoteErr) return rej('invalid renote param'); + mute: $.str.optional.note({ + default: 'mute_all' + }), - // Get 'media' parameter - const [media = null, mediaErr] = $.bool.optional.nullable.get(params.media); - if (mediaErr) return rej('invalid media param'); + reply: $.bool.optional.nullable.note({ + default: null, - // Get 'poll' parameter - const [poll = null, pollErr] = $.bool.optional.nullable.get(params.poll); - if (pollErr) return rej('invalid poll param'); + desc: { + 'ja-JP': '返信に限定するか否か' + } + }), - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate); - if (sinceDateErr) throw 'invalid sinceDate param'; + renote: $.bool.optional.nullable.note({ + default: null, - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate); - if (untilDateErr) throw 'invalid untilDate param'; + desc: { + 'ja-JP': 'Renoteに限定するか否か' + } + }), - // Get 'offset' parameter - const [offset = 0, offsetErr] = $.num.optional.min(0).get(params.offset); - if (offsetErr) return rej('invalid offset param'); + withFiles: $.bool.optional.note({ + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' + } + }), - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 30).get(params.limit); - if (limitErr) return rej('invalid limit param'); + media: $.bool.optional.nullable.note({ + default: null, + + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + } + }), + + poll: $.bool.optional.nullable.note({ + default: null, + + desc: { + 'ja-JP': 'アンケートが添付された投稿に限定するか否か' + } + }), - if (includeUserUsernames != null) { - const ids = (await Promise.all(includeUserUsernames.map(async (username) => { + untilId: $.type(ID).optional.note({ + desc: { + 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' + } + }), + + sinceDate: $.num.optional.note({ + }), + + untilDate: $.num.optional.note({ + }), + + offset: $.num.optional.min(0).note({ + default: 0 + }), + + limit: $.num.optional.range(1, 30).note({ + default: 10 + }), + } +}; + +export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; + + if (ps.includeUserUsernames != null) { + const ids = erase(null, await Promise.all(ps.includeUserUsernames.map(async (username) => { const _user = await User.findOne({ usernameLower: username.toLowerCase() }); return _user ? _user._id : null; - }))).filter(id => id != null); + }))); - ids.forEach(id => includeUserIds.push(id)); + ids.forEach(id => ps.includeUserIds.push(id)); } - if (excludeUserUsernames != null) { - const ids = (await Promise.all(excludeUserUsernames.map(async (username) => { + if (ps.excludeUserUsernames != null) { + const ids = erase(null, await Promise.all(ps.excludeUserUsernames.map(async (username) => { const _user = await User.findOne({ usernameLower: username.toLowerCase() }); return _user ? _user._id : null; - }))).filter(id => id != null); + }))); - ids.forEach(id => excludeUserIds.push(id)); + ids.forEach(id => ps.excludeUserIds.push(id)); } - let q: any = { - $and: [{ - tagsLower: tag.toLowerCase() - }] + const q: any = { + $and: [ps.tag ? { + tagsLower: ps.tag.toLowerCase() + } : { + $or: ps.query.map(tags => ({ + $and: tags.map(t => ({ + tagsLower: t.toLowerCase() + })) + })) + }], + deletedAt: { $exists: false } }; const push = (x: any) => q.$and.push(x); - if (includeUserIds && includeUserIds.length != 0) { + if (ps.includeUserIds && ps.includeUserIds.length != 0) { push({ userId: { - $in: includeUserIds + $in: ps.includeUserIds } }); - } else if (excludeUserIds && excludeUserIds.length != 0) { + } else if (ps.excludeUserIds && ps.excludeUserIds.length != 0) { push({ userId: { - $nin: excludeUserIds + $nin: ps.excludeUserIds } }); } - if (following != null && me != null) { + if (ps.following != null && me != null) { const ids = await getFriendIds(me._id, false); push({ - userId: following ? { + userId: ps.following ? { $in: ids } : { $nin: ids.concat(me._id) @@ -131,7 +182,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => }); const mutedUserIds = mutes.map(m => m.muteeId); - switch (mute) { + switch (ps.mute) { case 'mute_all': push({ userId: { @@ -202,8 +253,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } } - if (reply != null) { - if (reply) { + if (ps.reply != null) { + if (ps.reply) { push({ replyId: { $exists: true, @@ -223,8 +274,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } } - if (renote != null) { - if (renote) { + if (ps.renote != null) { + if (ps.renote) { push({ renoteId: { $exists: true, @@ -244,29 +295,16 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } } - if (media != null) { - if (media) { - push({ - mediaIds: { - $exists: true, - $ne: null - } - }); - } else { - push({ - $or: [{ - mediaIds: { - $exists: false - } - }, { - mediaIds: null - }] - }); - } + const withFiles = ps.withFiles != null ? ps.withFiles : ps.media; + + if (withFiles) { + push({ + fileIds: { $exists: true, $ne: [] } + }); } - if (poll != null) { - if (poll) { + if (ps.poll != null) { + if (ps.poll) { push({ poll: { $exists: true, @@ -286,24 +324,32 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } } - if (sinceDate) { + if (ps.untilId) { + push({ + _id: { + $lt: ps.untilId + } + }); + } + + if (ps.sinceDate) { push({ createdAt: { - $gt: new Date(sinceDate) + $gt: new Date(ps.sinceDate) } }); } - if (untilDate) { + if (ps.untilDate) { push({ createdAt: { - $lt: new Date(untilDate) + $lt: new Date(ps.untilDate) } }); } if (q.$and.length == 0) { - q = {}; + delete q.$and; } // Search notes @@ -312,10 +358,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => sort: { _id: -1 }, - limit: limit, - skip: offset + limit: ps.limit, + skip: ps.offset }); // Serialize - res(await Promise.all(notes.map(note => pack(note, me)))); + res(await packMany(notes, me)); }); diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 099bf2010b..44a504eb18 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -2,9 +2,10 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; import { getFriends } from '../../common/get-friends'; -import { pack } from '../../../../models/note'; +import { packMany } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; import getParams from '../../get-params'; +import { countIf } from '../../../../prelude/array'; export const meta = { desc: { @@ -67,9 +68,15 @@ export const meta = { } }), + withFiles: $.bool.optional.note({ + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' + } + }), + mediaOnly: $.bool.optional.note({ desc: { - 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' } }), } @@ -80,7 +87,7 @@ export default async (params: any, user: ILocalUser) => { if (psErr) throw psErr; // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { + if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } @@ -154,7 +161,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -170,7 +177,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -186,16 +193,18 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] }); } - if (ps.mediaOnly) { + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + + if (withFiles) { query.$and.push({ - mediaIds: { $exists: true, $ne: [] } + fileIds: { $exists: true, $ne: [] } }); } @@ -228,5 +237,5 @@ export default async (params: any, user: ILocalUser) => { }); // Serialize - return await Promise.all(timeline.map(note => pack(note, user))); + return await packMany(timeline, user); }; diff --git a/src/server/api/endpoints/notes/trend.ts b/src/server/api/endpoints/notes/trend.ts index 7a0a098f28..9f55ed3243 100644 --- a/src/server/api/endpoints/notes/trend.ts +++ b/src/server/api/endpoints/notes/trend.ts @@ -52,7 +52,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = } if (media != undefined) { - query.mediaIds = media ? { $exists: true, $ne: null } : null; + query.fileIds = media ? { $exists: true, $ne: null } : null; } if (poll != undefined) { diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts index a7b43014ed..6758b4eb73 100644 --- a/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/src/server/api/endpoints/notes/user-list-timeline.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; +import { packMany } from '../../../../models/note'; import UserList from '../../../../models/user-list'; import { ILocalUser } from '../../../../models/user'; import getParams from '../../get-params'; @@ -73,9 +73,15 @@ export const meta = { } }), + withFiles: $.bool.optional.note({ + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' + } + }), + mediaOnly: $.bool.optional.note({ desc: { - 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' } }), } @@ -160,7 +166,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -176,7 +182,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -192,16 +198,18 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] }); } - if (ps.mediaOnly) { + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + + if (withFiles) { query.$and.push({ - mediaIds: { $exists: true, $ne: [] } + fileIds: { $exists: true, $ne: [] } }); } @@ -234,5 +242,5 @@ export default async (params: any, user: ILocalUser) => { }); // Serialize - return await Promise.all(timeline.map(note => pack(note, user))); + return await packMany(timeline, user); }; diff --git a/src/server/api/endpoints/notifications/mark_all_as_read.ts b/src/server/api/endpoints/notifications/mark_all_as_read.ts index e2bde777b3..6487cd8b48 100644 --- a/src/server/api/endpoints/notifications/mark_all_as_read.ts +++ b/src/server/api/endpoints/notifications/mark_all_as_read.ts @@ -1,5 +1,5 @@ import Notification from '../../../../models/notification'; -import { publishUserStream } from '../../../../stream'; +import { publishMainStream } from '../../../../stream'; import User, { ILocalUser } from '../../../../models/user'; export const meta = { @@ -40,5 +40,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = }); // 全ての通知を読みましたよというイベントを発行 - publishUserStream(user._id, 'read_all_notifications'); + publishMainStream(user._id, 'readAllNotifications'); }); diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts index 3414600048..503fc94654 100644 --- a/src/server/api/endpoints/sw/register.ts +++ b/src/server/api/endpoints/sw/register.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import Subscription from '../../../../models/sw-subscription'; import { ILocalUser } from '../../../../models/user'; +import config from '../../../../config'; export const meta = { requireCredential: true @@ -31,8 +32,11 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, deletedAt: { $exists: false } }); - if (exist !== null) { - return res(); + if (exist != null) { + return res({ + state: 'already-subscribed', + key: config.sw.public_key + }); } await Subscription.insert({ @@ -42,5 +46,8 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, publickey: publickey }); - res(); + res({ + state: 'subscribed', + key: config.sw.public_key + }); }); diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts index 9411873573..7fe3ca9943 100644 --- a/src/server/api/endpoints/users/followers.ts +++ b/src/server/api/endpoints/users/followers.ts @@ -73,8 +73,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } // Serialize - const users = await Promise.all(following.map(async f => - await pack(f.followerId, me, { detail: true }))); + const users = await Promise.all(following.map(f => pack(f.followerId, me, { detail: true }))); // Response res({ diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts index 7a64d15d7b..0e564fd1b6 100644 --- a/src/server/api/endpoints/users/following.ts +++ b/src/server/api/endpoints/users/following.ts @@ -73,8 +73,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } // Serialize - const users = await Promise.all(following.map(async f => - await pack(f.followeeId, me, { detail: true }))); + const users = await Promise.all(following.map(f => pack(f.followeeId, me, { detail: true }))); // Response res({ diff --git a/src/server/api/endpoints/users/lists/delete.ts b/src/server/api/endpoints/users/lists/delete.ts new file mode 100644 index 0000000000..906534922e --- /dev/null +++ b/src/server/api/endpoints/users/lists/delete.ts @@ -0,0 +1,43 @@ +import $ from 'cafy'; +import ID from '../../../../../misc/cafy-id'; +import UserList, { deleteUserList } from '../../../../../models/user-list'; +import { ILocalUser } from '../../../../../models/user'; +import getParams from '../../../get-params'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーリストを削除します。', + 'en-US': 'Delete a user list' + }, + + requireCredential: true, + + kind: 'account-write', + + params: { + listId: $.type(ID).note({ + desc: { + 'ja-JP': '対象となるユーザーリストのID', + 'en-US': 'ID of target user list' + } + }) + } +}; + +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + const userList = await UserList.findOne({ + _id: ps.listId, + userId: user._id + }); + + if (userList == null) { + return rej('list not found'); + } + + deleteUserList(userList); + + res(); +}); diff --git a/src/server/api/endpoints/users/lists/update.ts b/src/server/api/endpoints/users/lists/update.ts new file mode 100644 index 0000000000..e6577eca4f --- /dev/null +++ b/src/server/api/endpoints/users/lists/update.ts @@ -0,0 +1,56 @@ +import $ from 'cafy'; +import ID from '../../../../../misc/cafy-id'; +import UserList, { pack } from '../../../../../models/user-list'; +import { ILocalUser } from '../../../../../models/user'; +import getParams from '../../../get-params'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーリストを更新します。', + 'en-US': 'Update a user list' + }, + + requireCredential: true, + + kind: 'account-write', + + params: { + listId: $.type(ID).note({ + desc: { + 'ja-JP': '対象となるユーザーリストのID', + 'en-US': 'ID of target user list' + } + }), + title: $.str.range(1, 100).note({ + desc: { + 'ja-JP': 'このユーザーリストの名前', + 'en-US': 'name of this user list' + } + }) + } +}; + +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; + + // Fetch the list + const userList = await UserList.findOne({ + _id: ps.listId, + userId: user._id + }); + + if (userList == null) { + return rej('list not found'); + } + + // update + await UserList.update({ _id: userList._id }, { + $set: { + title: ps.title + } + }); + + // Response + res(await pack(userList._id)); +}); diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts index ff7855bde0..1bfe832c51 100644 --- a/src/server/api/endpoints/users/notes.ts +++ b/src/server/api/endpoints/users/notes.ts @@ -1,64 +1,123 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import getHostLower from '../../common/get-host-lower'; -import Note, { pack } from '../../../../models/note'; +import Note, { packMany } from '../../../../models/note'; import User, { ILocalUser } from '../../../../models/user'; +import getParams from '../../get-params'; +import { countIf } from '../../../../prelude/array'; -/** - * Get notes of a user - */ -export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { - // Get 'userId' parameter - const [userId, userIdErr] = $.type(ID).optional.get(params.userId); - if (userIdErr) return rej('invalid userId param'); +export const meta = { + desc: { + 'ja-JP': '指定したユーザーのタイムラインを取得します。' + }, - // Get 'username' parameter - const [username, usernameErr] = $.str.optional.get(params.username); - if (usernameErr) return rej('invalid username param'); + params: { + userId: $.type(ID).optional.note({ + desc: { + 'ja-JP': 'ユーザーID' + } + }), - if (userId === undefined && username === undefined) { - return rej('userId or username is required'); - } + username: $.str.optional.note({ + desc: { + 'ja-JP': 'ユーザー名' + } + }), + + host: $.str.optional.note({ + }), - // Get 'host' parameter - const [host, hostErr] = $.str.optional.get(params.host); - if (hostErr) return rej('invalid host param'); + includeReplies: $.bool.optional.note({ + default: true, - // Get 'includeReplies' parameter - const [includeReplies = true, includeRepliesErr] = $.bool.optional.get(params.includeReplies); - if (includeRepliesErr) return rej('invalid includeReplies param'); + desc: { + 'ja-JP': 'リプライを含めるか否か' + } + }), - // Get 'withMedia' parameter - const [withMedia = false, withMediaErr] = $.bool.optional.get(params.withMedia); - if (withMediaErr) return rej('invalid withMedia param'); + limit: $.num.optional.range(1, 100).note({ + default: 10, + desc: { + 'ja-JP': '最大数' + } + }), - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) return rej('invalid limit param'); + sinceId: $.type(ID).optional.note({ + desc: { + 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します' + } + }), - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); - if (sinceIdErr) return rej('invalid sinceId param'); + untilId: $.type(ID).optional.note({ + desc: { + 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' + } + }), - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); - if (untilIdErr) return rej('invalid untilId param'); + sinceDate: $.num.optional.note({ + desc: { + 'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + } + }), - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate); - if (sinceDateErr) throw 'invalid sinceDate param'; + untilDate: $.num.optional.note({ + desc: { + 'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + } + }), - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate); - if (untilDateErr) throw 'invalid untilDate param'; + includeMyRenotes: $.bool.optional.note({ + default: true, + desc: { + 'ja-JP': '自分の行ったRenoteを含めるかどうか' + } + }), + + includeRenotedMyNotes: $.bool.optional.note({ + default: true, + desc: { + 'ja-JP': 'Renoteされた自分の投稿を含めるかどうか' + } + }), + + includeLocalRenotes: $.bool.optional.note({ + default: true, + desc: { + 'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか' + } + }), + + withFiles: $.bool.optional.note({ + default: false, + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' + } + }), + + mediaOnly: $.bool.optional.note({ + default: false, + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + } + }), + } +}; + +export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; + + if (ps.userId === undefined && ps.username === undefined) { + return rej('userId or username is required'); + } // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } - const q = userId !== undefined - ? { _id: userId } - : { usernameLower: username.toLowerCase(), host: getHostLower(host) } ; + const q = ps.userId !== undefined + ? { _id: ps.userId } + : { usernameLower: ps.username.toLowerCase(), host: getHostLower(ps.host) } ; // Lookup user const user = await User.findOne(q, { @@ -80,32 +139,34 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => userId: user._id } as any; - if (sinceId) { + if (ps.sinceId) { sort._id = 1; query._id = { - $gt: sinceId + $gt: ps.sinceId }; - } else if (untilId) { + } else if (ps.untilId) { query._id = { - $lt: untilId + $lt: ps.untilId }; - } else if (sinceDate) { + } else if (ps.sinceDate) { sort._id = 1; query.createdAt = { - $gt: new Date(sinceDate) + $gt: new Date(ps.sinceDate) }; - } else if (untilDate) { + } else if (ps.untilDate) { query.createdAt = { - $lt: new Date(untilDate) + $lt: new Date(ps.untilDate) }; } - if (!includeReplies) { + if (!ps.includeReplies) { query.replyId = null; } - if (withMedia) { - query.mediaIds = { + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + + if (withFiles) { + query.fileIds = { $exists: true, $ne: [] }; @@ -115,12 +176,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => // Issue query const notes = await Note .find(query, { - limit: limit, + limit: ps.limit, sort: sort }); // Serialize - res(await Promise.all(notes.map(async (note) => - await pack(note, me) - ))); + res(await packMany(notes, me)); }); diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index c42fb7bd8c..0e44c2ddd6 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import User, { ILocalUser } from '../../../models/user'; import Signin, { pack } from '../../../models/signin'; -import { publishUserStream } from '../../../stream'; +import { publishMainStream } from '../../../stream'; import signin from '../common/signin'; import config from '../../../config'; @@ -87,5 +87,5 @@ export default async (ctx: Koa.Context) => { }); // Publish signin event - publishUserStream(user._id, 'signin', await pack(record)); + publishMainStream(user._id, 'signin', await pack(record)); }; diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts index aad2846bb4..f71e588628 100644 --- a/src/server/api/service/twitter.ts +++ b/src/server/api/service/twitter.ts @@ -4,7 +4,7 @@ import * as uuid from 'uuid'; import autwh from 'autwh'; import redis from '../../../db/redis'; import User, { pack, ILocalUser } from '../../../models/user'; -import { publishUserStream } from '../../../stream'; +import { publishMainStream } from '../../../stream'; import config from '../../../config'; import signin from '../common/signin'; @@ -49,7 +49,7 @@ router.get('/disconnect/twitter', async ctx => { ctx.body = `Twitterの連携を解除しました :v:`; // Publish i updated event - publishUserStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user._id, 'meUpdated', await pack(user, user, { detail: true, includeSecrets: true })); @@ -174,7 +174,7 @@ if (config.twitter == null) { ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; // Publish i updated event - publishUserStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user._id, 'meUpdated', await pack(user, user, { detail: true, includeSecrets: true })); diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts new file mode 100644 index 0000000000..e2726060dc --- /dev/null +++ b/src/server/api/stream/channel.ts @@ -0,0 +1,39 @@ +import autobind from 'autobind-decorator'; +import Connection from '.'; + +/** + * Stream channel + */ +export default abstract class Channel { + protected connection: Connection; + public id: string; + + protected get user() { + return this.connection.user; + } + + protected get subscriber() { + return this.connection.subscriber; + } + + constructor(id: string, connection: Connection) { + this.id = id; + this.connection = connection; + } + + @autobind + public send(typeOrPayload: any, payload?: any) { + const type = payload === undefined ? typeOrPayload.type : typeOrPayload; + const body = payload === undefined ? typeOrPayload.body : payload; + + this.connection.sendMessageToWs('channel', { + id: this.id, + type: type, + body: body + }); + } + + public abstract init(params: any): void; + public dispose?(): void; + public onMessage?(type: string, body: any): void; +} diff --git a/src/server/api/stream/channels/drive.ts b/src/server/api/stream/channels/drive.ts new file mode 100644 index 0000000000..807fc93cd0 --- /dev/null +++ b/src/server/api/stream/channels/drive.ts @@ -0,0 +1,12 @@ +import autobind from 'autobind-decorator'; +import Channel from '../channel'; + +export default class extends Channel { + @autobind + public async init(params: any) { + // Subscribe drive stream + this.subscriber.on(`driveStream:${this.user._id}`, data => { + this.send(data); + }); + } +} diff --git a/src/server/api/stream/channels/games/reversi-game.ts b/src/server/api/stream/channels/games/reversi-game.ts new file mode 100644 index 0000000000..11f1fb1feb --- /dev/null +++ b/src/server/api/stream/channels/games/reversi-game.ts @@ -0,0 +1,309 @@ +import autobind from 'autobind-decorator'; +import * as CRC32 from 'crc-32'; +import ReversiGame, { pack } from '../../../../../models/games/reversi/game'; +import { publishReversiGameStream } from '../../../../../stream'; +import Reversi from '../../../../../games/reversi/core'; +import * as maps from '../../../../../games/reversi/maps'; +import Channel from '../../channel'; + +export default class extends Channel { + private gameId: string; + + @autobind + public async init(params: any) { + this.gameId = params.gameId as string; + + // Subscribe game stream + this.subscriber.on(`reversiGameStream:${this.gameId}`, data => { + this.send(data); + }); + } + + @autobind + public onMessage(type: string, body: any) { + switch (type) { + case 'accept': this.accept(true); break; + case 'cancel-accept': this.accept(false); break; + case 'update-settings': this.updateSettings(body.settings); break; + case 'init-form': this.initForm(body); break; + case 'update-form': this.updateForm(body.id, body.value); break; + case 'message': this.message(body); break; + case 'set': this.set(body.pos); break; + case 'check': this.check(body.crc32); break; + } + } + + @autobind + private async updateSettings(settings: any) { + const game = await ReversiGame.findOne({ _id: this.gameId }); + + if (game.isStarted) return; + if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; + if (game.user1Id.equals(this.user._id) && game.user1Accepted) return; + if (game.user2Id.equals(this.user._id) && game.user2Accepted) return; + + await ReversiGame.update({ _id: this.gameId }, { + $set: { + settings + } + }); + + publishReversiGameStream(this.gameId, 'updateSettings', settings); + } + + @autobind + private async initForm(form: any) { + const game = await ReversiGame.findOne({ _id: this.gameId }); + + if (game.isStarted) return; + if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; + + const set = game.user1Id.equals(this.user._id) ? { + form1: form + } : { + form2: form + }; + + await ReversiGame.update({ _id: this.gameId }, { + $set: set + }); + + publishReversiGameStream(this.gameId, 'initForm', { + userId: this.user._id, + form + }); + } + + @autobind + private async updateForm(id: string, value: any) { + const game = await ReversiGame.findOne({ _id: this.gameId }); + + if (game.isStarted) return; + if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; + + const form = game.user1Id.equals(this.user._id) ? game.form2 : game.form1; + + const item = form.find((i: any) => i.id == id); + + if (item == null) return; + + item.value = value; + + const set = game.user1Id.equals(this.user._id) ? { + form2: form + } : { + form1: form + }; + + await ReversiGame.update({ _id: this.gameId }, { + $set: set + }); + + publishReversiGameStream(this.gameId, 'updateForm', { + userId: this.user._id, + id, + value + }); + } + + @autobind + private async message(message: any) { + message.id = Math.random(); + publishReversiGameStream(this.gameId, 'message', { + userId: this.user._id, + message + }); + } + + @autobind + private async accept(accept: boolean) { + const game = await ReversiGame.findOne({ _id: this.gameId }); + + if (game.isStarted) return; + + let bothAccepted = false; + + if (game.user1Id.equals(this.user._id)) { + await ReversiGame.update({ _id: this.gameId }, { + $set: { + user1Accepted: accept + } + }); + + publishReversiGameStream(this.gameId, 'changeAccepts', { + user1: accept, + user2: game.user2Accepted + }); + + if (accept && game.user2Accepted) bothAccepted = true; + } else if (game.user2Id.equals(this.user._id)) { + await ReversiGame.update({ _id: this.gameId }, { + $set: { + user2Accepted: accept + } + }); + + publishReversiGameStream(this.gameId, 'changeAccepts', { + user1: game.user1Accepted, + user2: accept + }); + + if (accept && game.user1Accepted) bothAccepted = true; + } else { + return; + } + + if (bothAccepted) { + // 3秒後、まだacceptされていたらゲーム開始 + setTimeout(async () => { + const freshGame = await ReversiGame.findOne({ _id: this.gameId }); + if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; + if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; + + let bw: number; + if (freshGame.settings.bw == 'random') { + bw = Math.random() > 0.5 ? 1 : 2; + } else { + bw = freshGame.settings.bw as number; + } + + function getRandomMap() { + const mapCount = Object.entries(maps).length; + const rnd = Math.floor(Math.random() * mapCount); + return Object.values(maps)[rnd].data; + } + + const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); + + await ReversiGame.update({ _id: this.gameId }, { + $set: { + startedAt: new Date(), + isStarted: true, + black: bw, + 'settings.map': map + } + }); + + //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 + const o = new Reversi(map, { + isLlotheo: freshGame.settings.isLlotheo, + canPutEverywhere: freshGame.settings.canPutEverywhere, + loopedBoard: freshGame.settings.loopedBoard + }); + + if (o.isEnded) { + let winner; + if (o.winner === true) { + winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; + } else if (o.winner === false) { + winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; + } else { + winner = null; + } + + await ReversiGame.update({ + _id: this.gameId + }, { + $set: { + isEnded: true, + winnerId: winner + } + }); + + publishReversiGameStream(this.gameId, 'ended', { + winnerId: winner, + game: await pack(this.gameId, this.user) + }); + } + //#endregion + + publishReversiGameStream(this.gameId, 'started', await pack(this.gameId, this.user)); + }, 3000); + } + } + + // 石を打つ + @autobind + private async set(pos: number) { + const game = await ReversiGame.findOne({ _id: this.gameId }); + + if (!game.isStarted) return; + if (game.isEnded) return; + if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; + + const o = new Reversi(game.settings.map, { + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard + }); + + game.logs.forEach(log => { + o.put(log.color, log.pos); + }); + + const myColor = + (game.user1Id.equals(this.user._id) && game.black == 1) || (game.user2Id.equals(this.user._id) && game.black == 2) + ? true + : false; + + if (!o.canPut(myColor, pos)) return; + o.put(myColor, pos); + + let winner; + if (o.isEnded) { + if (o.winner === true) { + winner = game.black == 1 ? game.user1Id : game.user2Id; + } else if (o.winner === false) { + winner = game.black == 1 ? game.user2Id : game.user1Id; + } else { + winner = null; + } + } + + const log = { + at: new Date(), + color: myColor, + pos + }; + + const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); + + await ReversiGame.update({ + _id: this.gameId + }, { + $set: { + crc32, + isEnded: o.isEnded, + winnerId: winner + }, + $push: { + logs: log + } + }); + + publishReversiGameStream(this.gameId, 'set', Object.assign(log, { + next: o.turn + })); + + if (o.isEnded) { + publishReversiGameStream(this.gameId, 'ended', { + winnerId: winner, + game: await pack(this.gameId, this.user) + }); + } + } + + @autobind + private async check(crc32: string) { + const game = await ReversiGame.findOne({ _id: this.gameId }); + + if (!game.isStarted) return; + + // 互換性のため + if (game.crc32 == null) return; + + if (crc32 !== game.crc32) { + this.send('rescue', await pack(game, this.user)); + } + } +} diff --git a/src/server/api/stream/channels/games/reversi.ts b/src/server/api/stream/channels/games/reversi.ts new file mode 100644 index 0000000000..d75025c944 --- /dev/null +++ b/src/server/api/stream/channels/games/reversi.ts @@ -0,0 +1,30 @@ +import autobind from 'autobind-decorator'; +import * as mongo from 'mongodb'; +import Matching, { pack } from '../../../../../models/games/reversi/matching'; +import { publishMainStream } from '../../../../../stream'; +import Channel from '../../channel'; + +export default class extends Channel { + @autobind + public async init(params: any) { + // Subscribe reversi stream + this.subscriber.on(`reversiStream:${this.user._id}`, data => { + this.send(data); + }); + } + + @autobind + public async onMessage(type: string, body: any) { + switch (type) { + case 'ping': + if (body.id == null) return; + const matching = await Matching.findOne({ + parentId: this.user._id, + childId: new mongo.ObjectID(body.id) + }); + if (matching == null) return; + publishMainStream(matching.childId, 'reversiInvited', await pack(matching, matching.childId)); + break; + } + } +} diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts new file mode 100644 index 0000000000..ab0fe5d094 --- /dev/null +++ b/src/server/api/stream/channels/global-timeline.ts @@ -0,0 +1,39 @@ +import autobind from 'autobind-decorator'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/note'; +import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; +import Channel from '../channel'; + +export default class extends Channel { + private mutedUserIds: string[] = []; + + @autobind + public async init(params: any) { + // Subscribe events + this.subscriber.on('globalTimeline', this.onNote); + + const mute = await Mute.find({ muterId: this.user._id }); + this.mutedUserIds = mute.map(m => m.muteeId.toString()); + } + + @autobind + private async onNote(note: any) { + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await pack(note.renoteId, this.user, { + detail: true + }); + } + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (shouldMuteThisNote(note, this.mutedUserIds)) return; + + this.send('note', note); + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off('globalTimeline', this.onNote); + } +} diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts new file mode 100644 index 0000000000..652b0caa5b --- /dev/null +++ b/src/server/api/stream/channels/hashtag.ts @@ -0,0 +1,33 @@ +import autobind from 'autobind-decorator'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/note'; +import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; +import Channel from '../channel'; + +export default class extends Channel { + @autobind + public async init(params: any) { + const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null; + const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; + + const q: Array<string[]> = params.q; + + // Subscribe stream + this.subscriber.on('hashtag', async note => { + const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase()))); + if (!matched) return; + + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await pack(note.renoteId, this.user, { + detail: true + }); + } + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (shouldMuteThisNote(note, mutedUserIds)) return; + + this.send('note', note); + }); + } +} diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts new file mode 100644 index 0000000000..4c674e75ef --- /dev/null +++ b/src/server/api/stream/channels/home-timeline.ts @@ -0,0 +1,39 @@ +import autobind from 'autobind-decorator'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/note'; +import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; +import Channel from '../channel'; + +export default class extends Channel { + private mutedUserIds: string[] = []; + + @autobind + public async init(params: any) { + // Subscribe events + this.subscriber.on(`homeTimeline:${this.user._id}`, this.onNote); + + const mute = await Mute.find({ muterId: this.user._id }); + this.mutedUserIds = mute.map(m => m.muteeId.toString()); + } + + @autobind + private async onNote(note: any) { + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await pack(note.renoteId, this.user, { + detail: true + }); + } + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (shouldMuteThisNote(note, this.mutedUserIds)) return; + + this.send('note', note); + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off(`homeTimeline:${this.user._id}`, this.onNote); + } +} diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts new file mode 100644 index 0000000000..0b12ab3a8f --- /dev/null +++ b/src/server/api/stream/channels/hybrid-timeline.ts @@ -0,0 +1,41 @@ +import autobind from 'autobind-decorator'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/note'; +import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; +import Channel from '../channel'; + +export default class extends Channel { + private mutedUserIds: string[] = []; + + @autobind + public async init(params: any) { + // Subscribe events + this.subscriber.on('hybridTimeline', this.onNewNote); + this.subscriber.on(`hybridTimeline:${this.user._id}`, this.onNewNote); + + const mute = await Mute.find({ muterId: this.user._id }); + this.mutedUserIds = mute.map(m => m.muteeId.toString()); + } + + @autobind + private async onNewNote(note: any) { + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await pack(note.renoteId, this.user, { + detail: true + }); + } + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (shouldMuteThisNote(note, this.mutedUserIds)) return; + + this.send('note', note); + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off('hybridTimeline', this.onNewNote); + this.subscriber.off(`hybridTimeline:${this.user._id}`, this.onNewNote); + } +} diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts new file mode 100644 index 0000000000..7e71590d00 --- /dev/null +++ b/src/server/api/stream/channels/index.ts @@ -0,0 +1,31 @@ +import main from './main'; +import homeTimeline from './home-timeline'; +import localTimeline from './local-timeline'; +import hybridTimeline from './hybrid-timeline'; +import globalTimeline from './global-timeline'; +import notesStats from './notes-stats'; +import serverStats from './server-stats'; +import userList from './user-list'; +import messaging from './messaging'; +import messagingIndex from './messaging-index'; +import drive from './drive'; +import hashtag from './hashtag'; +import gamesReversi from './games/reversi'; +import gamesReversiGame from './games/reversi-game'; + +export default { + main, + homeTimeline, + localTimeline, + hybridTimeline, + globalTimeline, + notesStats, + serverStats, + userList, + messaging, + messagingIndex, + drive, + hashtag, + gamesReversi, + gamesReversiGame +}; diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts new file mode 100644 index 0000000000..769ec6392f --- /dev/null +++ b/src/server/api/stream/channels/local-timeline.ts @@ -0,0 +1,39 @@ +import autobind from 'autobind-decorator'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/note'; +import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; +import Channel from '../channel'; + +export default class extends Channel { + private mutedUserIds: string[] = []; + + @autobind + public async init(params: any) { + // Subscribe events + this.subscriber.on('localTimeline', this.onNote); + + const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null; + this.mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; + } + + @autobind + private async onNote(note: any) { + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await pack(note.renoteId, this.user, { + detail: true + }); + } + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (shouldMuteThisNote(note, this.mutedUserIds)) return; + + this.send('note', note); + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off('localTimeline', this.onNote); + } +} diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts new file mode 100644 index 0000000000..fd0984c833 --- /dev/null +++ b/src/server/api/stream/channels/main.ts @@ -0,0 +1,25 @@ +import autobind from 'autobind-decorator'; +import Mute from '../../../../models/mute'; +import Channel from '../channel'; + +export default class extends Channel { + @autobind + public async init(params: any) { + const mute = await Mute.find({ muterId: this.user._id }); + const mutedUserIds = mute.map(m => m.muteeId.toString()); + + // Subscribe main stream channel + this.subscriber.on(`mainStream:${this.user._id}`, async data => { + const { type, body } = data; + + switch (type) { + case 'notification': { + if (mutedUserIds.includes(body.userId)) return; + break; + } + } + + this.send(type, body); + }); + } +} diff --git a/src/server/api/stream/channels/messaging-index.ts b/src/server/api/stream/channels/messaging-index.ts new file mode 100644 index 0000000000..6e87cca7f4 --- /dev/null +++ b/src/server/api/stream/channels/messaging-index.ts @@ -0,0 +1,12 @@ +import autobind from 'autobind-decorator'; +import Channel from '../channel'; + +export default class extends Channel { + @autobind + public async init(params: any) { + // Subscribe messaging index stream + this.subscriber.on(`messagingIndexStream:${this.user._id}`, data => { + this.send(data); + }); + } +} diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts new file mode 100644 index 0000000000..e1a78c8678 --- /dev/null +++ b/src/server/api/stream/channels/messaging.ts @@ -0,0 +1,26 @@ +import autobind from 'autobind-decorator'; +import read from '../../common/read-messaging-message'; +import Channel from '../channel'; + +export default class extends Channel { + private otherpartyId: string; + + @autobind + public async init(params: any) { + this.otherpartyId = params.otherparty as string; + + // Subscribe messaging stream + this.subscriber.on(`messagingStream:${this.user._id}-${this.otherpartyId}`, data => { + this.send(data); + }); + } + + @autobind + public onMessage(type: string, body: any) { + switch (type) { + case 'read': + read(this.user._id, this.otherpartyId, body.id); + break; + } + } +} diff --git a/src/server/api/stream/channels/notes-stats.ts b/src/server/api/stream/channels/notes-stats.ts new file mode 100644 index 0000000000..cc68d9886d --- /dev/null +++ b/src/server/api/stream/channels/notes-stats.ts @@ -0,0 +1,34 @@ +import autobind from 'autobind-decorator'; +import Xev from 'xev'; +import Channel from '../channel'; + +const ev = new Xev(); + +export default class extends Channel { + @autobind + public async init(params: any) { + ev.addListener('notesStats', this.onStats); + } + + @autobind + private onStats(stats: any) { + this.send('stats', stats); + } + + @autobind + public onMessage(type: string, body: any) { + switch (type) { + case 'requestLog': + ev.once(`notesStatsLog:${body.id}`, statsLog => { + this.send('statsLog', statsLog); + }); + ev.emit('requestNotesStatsLog', body.id); + break; + } + } + + @autobind + public dispose() { + ev.removeListener('notesStats', this.onStats); + } +} diff --git a/src/server/api/stream/channels/server-stats.ts b/src/server/api/stream/channels/server-stats.ts new file mode 100644 index 0000000000..28a566e8ae --- /dev/null +++ b/src/server/api/stream/channels/server-stats.ts @@ -0,0 +1,37 @@ +import autobind from 'autobind-decorator'; +import Xev from 'xev'; +import Channel from '../channel'; + +const ev = new Xev(); + +export default class extends Channel { + @autobind + public async init(params: any) { + ev.addListener('serverStats', this.onStats); + } + + @autobind + private onStats(stats: any) { + this.send('stats', stats); + } + + @autobind + public onMessage(type: string, body: any) { + switch (type) { + case 'requestLog': + ev.once(`serverStatsLog:${body.id}`, statsLog => { + this.send('statsLog', statsLog); + }); + ev.emit('requestServerStatsLog', { + id: body.id, + length: body.length + }); + break; + } + } + + @autobind + public dispose() { + ev.removeListener('serverStats', this.onStats); + } +} diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts new file mode 100644 index 0000000000..4ace308923 --- /dev/null +++ b/src/server/api/stream/channels/user-list.ts @@ -0,0 +1,14 @@ +import autobind from 'autobind-decorator'; +import Channel from '../channel'; + +export default class extends Channel { + @autobind + public async init(params: any) { + const listId = params.listId as string; + + // Subscribe stream + this.subscriber.on(`userListStream:${listId}`, data => { + this.send(data); + }); + } +} diff --git a/src/server/api/stream/drive.ts b/src/server/api/stream/drive.ts deleted file mode 100644 index 28c241e1bc..0000000000 --- a/src/server/api/stream/drive.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; - -export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void { - // Subscribe drive stream - subscriber.on(`drive-stream:${user._id}`, data => { - connection.send(JSON.stringify(data)); - }); -} diff --git a/src/server/api/stream/games/reversi-game.ts b/src/server/api/stream/games/reversi-game.ts deleted file mode 100644 index 5cbbf42d59..0000000000 --- a/src/server/api/stream/games/reversi-game.ts +++ /dev/null @@ -1,332 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; -import * as CRC32 from 'crc-32'; -import ReversiGame, { pack } from '../../../../models/games/reversi/game'; -import { publishReversiGameStream } from '../../../../stream'; -import Reversi from '../../../../games/reversi/core'; -import * as maps from '../../../../games/reversi/maps'; -import { ParsedUrlQuery } from 'querystring'; - -export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user?: any): void { - const q = request.resourceURL.query as ParsedUrlQuery; - const gameId = q.game as string; - - // Subscribe game stream - subscriber.on(`reversi-game-stream:${gameId}`, data => { - connection.send(JSON.stringify(data)); - }); - - connection.on('message', async (data) => { - const msg = JSON.parse(data.utf8Data); - - switch (msg.type) { - case 'accept': - accept(true); - break; - - case 'cancel-accept': - accept(false); - break; - - case 'update-settings': - if (msg.settings == null) return; - updateSettings(msg.settings); - break; - - case 'init-form': - if (msg.body == null) return; - initForm(msg.body); - break; - - case 'update-form': - if (msg.id == null || msg.value === undefined) return; - updateForm(msg.id, msg.value); - break; - - case 'message': - if (msg.body == null) return; - message(msg.body); - break; - - case 'set': - if (msg.pos == null) return; - set(msg.pos); - break; - - case 'check': - if (msg.crc32 == null) return; - check(msg.crc32); - break; - } - }); - - async function updateSettings(settings: any) { - const game = await ReversiGame.findOne({ _id: gameId }); - - if (game.isStarted) return; - if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; - if (game.user1Id.equals(user._id) && game.user1Accepted) return; - if (game.user2Id.equals(user._id) && game.user2Accepted) return; - - await ReversiGame.update({ _id: gameId }, { - $set: { - settings - } - }); - - publishReversiGameStream(gameId, 'update-settings', settings); - } - - async function initForm(form: any) { - const game = await ReversiGame.findOne({ _id: gameId }); - - if (game.isStarted) return; - if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; - - const set = game.user1Id.equals(user._id) ? { - form1: form - } : { - form2: form - }; - - await ReversiGame.update({ _id: gameId }, { - $set: set - }); - - publishReversiGameStream(gameId, 'init-form', { - userId: user._id, - form - }); - } - - async function updateForm(id: string, value: any) { - const game = await ReversiGame.findOne({ _id: gameId }); - - if (game.isStarted) return; - if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; - - const form = game.user1Id.equals(user._id) ? game.form2 : game.form1; - - const item = form.find((i: any) => i.id == id); - - if (item == null) return; - - item.value = value; - - const set = game.user1Id.equals(user._id) ? { - form2: form - } : { - form1: form - }; - - await ReversiGame.update({ _id: gameId }, { - $set: set - }); - - publishReversiGameStream(gameId, 'update-form', { - userId: user._id, - id, - value - }); - } - - async function message(message: any) { - message.id = Math.random(); - publishReversiGameStream(gameId, 'message', { - userId: user._id, - message - }); - } - - async function accept(accept: boolean) { - const game = await ReversiGame.findOne({ _id: gameId }); - - if (game.isStarted) return; - - let bothAccepted = false; - - if (game.user1Id.equals(user._id)) { - await ReversiGame.update({ _id: gameId }, { - $set: { - user1Accepted: accept - } - }); - - publishReversiGameStream(gameId, 'change-accepts', { - user1: accept, - user2: game.user2Accepted - }); - - if (accept && game.user2Accepted) bothAccepted = true; - } else if (game.user2Id.equals(user._id)) { - await ReversiGame.update({ _id: gameId }, { - $set: { - user2Accepted: accept - } - }); - - publishReversiGameStream(gameId, 'change-accepts', { - user1: game.user1Accepted, - user2: accept - }); - - if (accept && game.user1Accepted) bothAccepted = true; - } else { - return; - } - - if (bothAccepted) { - // 3秒後、まだacceptされていたらゲーム開始 - setTimeout(async () => { - const freshGame = await ReversiGame.findOne({ _id: gameId }); - if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; - if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; - - let bw: number; - if (freshGame.settings.bw == 'random') { - bw = Math.random() > 0.5 ? 1 : 2; - } else { - bw = freshGame.settings.bw as number; - } - - function getRandomMap() { - const mapCount = Object.entries(maps).length; - const rnd = Math.floor(Math.random() * mapCount); - return Object.values(maps)[rnd].data; - } - - const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); - - await ReversiGame.update({ _id: gameId }, { - $set: { - startedAt: new Date(), - isStarted: true, - black: bw, - 'settings.map': map - } - }); - - //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - const o = new Reversi(map, { - isLlotheo: freshGame.settings.isLlotheo, - canPutEverywhere: freshGame.settings.canPutEverywhere, - loopedBoard: freshGame.settings.loopedBoard - }); - - if (o.isEnded) { - let winner; - if (o.winner === true) { - winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; - } else if (o.winner === false) { - winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; - } else { - winner = null; - } - - await ReversiGame.update({ - _id: gameId - }, { - $set: { - isEnded: true, - winnerId: winner - } - }); - - publishReversiGameStream(gameId, 'ended', { - winnerId: winner, - game: await pack(gameId, user) - }); - } - //#endregion - - publishReversiGameStream(gameId, 'started', await pack(gameId, user)); - }, 3000); - } - } - - // 石を打つ - async function set(pos: number) { - const game = await ReversiGame.findOne({ _id: gameId }); - - if (!game.isStarted) return; - if (game.isEnded) return; - if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; - - const o = new Reversi(game.settings.map, { - isLlotheo: game.settings.isLlotheo, - canPutEverywhere: game.settings.canPutEverywhere, - loopedBoard: game.settings.loopedBoard - }); - - game.logs.forEach(log => { - o.put(log.color, log.pos); - }); - - const myColor = - (game.user1Id.equals(user._id) && game.black == 1) || (game.user2Id.equals(user._id) && game.black == 2) - ? true - : false; - - if (!o.canPut(myColor, pos)) return; - o.put(myColor, pos); - - let winner; - if (o.isEnded) { - if (o.winner === true) { - winner = game.black == 1 ? game.user1Id : game.user2Id; - } else if (o.winner === false) { - winner = game.black == 1 ? game.user2Id : game.user1Id; - } else { - winner = null; - } - } - - const log = { - at: new Date(), - color: myColor, - pos - }; - - const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); - - await ReversiGame.update({ - _id: gameId - }, { - $set: { - crc32, - isEnded: o.isEnded, - winnerId: winner - }, - $push: { - logs: log - } - }); - - publishReversiGameStream(gameId, 'set', Object.assign(log, { - next: o.turn - })); - - if (o.isEnded) { - publishReversiGameStream(gameId, 'ended', { - winnerId: winner, - game: await pack(gameId, user) - }); - } - } - - async function check(crc32: string) { - const game = await ReversiGame.findOne({ _id: gameId }); - - if (!game.isStarted) return; - - // 互換性のため - if (game.crc32 == null) return; - - if (crc32 !== game.crc32) { - connection.send(JSON.stringify({ - type: 'rescue', - body: await pack(game, user) - })); - } - } -} diff --git a/src/server/api/stream/games/reversi.ts b/src/server/api/stream/games/reversi.ts deleted file mode 100644 index f467613b21..0000000000 --- a/src/server/api/stream/games/reversi.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as mongo from 'mongodb'; -import * as websocket from 'websocket'; -import Xev from 'xev'; -import Matching, { pack } from '../../../../models/games/reversi/matching'; -import { publishUserStream } from '../../../../stream'; - -export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void { - // Subscribe reversi stream - subscriber.on(`reversi-stream:${user._id}`, data => { - connection.send(JSON.stringify(data)); - }); - - connection.on('message', async (data) => { - const msg = JSON.parse(data.utf8Data); - - switch (msg.type) { - case 'ping': - if (msg.id == null) return; - const matching = await Matching.findOne({ - parentId: user._id, - childId: new mongo.ObjectID(msg.id) - }); - if (matching == null) return; - publishUserStream(matching.childId, 'reversi_invited', await pack(matching, matching.childId)); - break; - } - }); -} diff --git a/src/server/api/stream/global-timeline.ts b/src/server/api/stream/global-timeline.ts deleted file mode 100644 index 4786450cbb..0000000000 --- a/src/server/api/stream/global-timeline.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; - -import { IUser } from '../../../models/user'; -import Mute from '../../../models/mute'; - -export default async function( - request: websocket.request, - connection: websocket.connection, - subscriber: Xev, - user: IUser -) { - const mute = await Mute.find({ muterId: user._id }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); - - // Subscribe stream - subscriber.on('global-timeline', async note => { - //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (mutedUserIds.indexOf(note.userId) != -1) { - return; - } - if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) { - return; - } - if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) { - return; - } - //#endregion - - connection.send(JSON.stringify({ - type: 'note', - body: note - })); - }); -} diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts deleted file mode 100644 index dc3ce9d19f..0000000000 --- a/src/server/api/stream/home.ts +++ /dev/null @@ -1,113 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; -import * as debug from 'debug'; - -import User, { IUser } from '../../../models/user'; -import Mute from '../../../models/mute'; -import { pack as packNote, pack } from '../../../models/note'; -import readNotification from '../common/read-notification'; -import call from '../call'; -import { IApp } from '../../../models/app'; - -const log = debug('misskey'); - -export default async function( - request: websocket.request, - connection: websocket.connection, - subscriber: Xev, - user: IUser, - app: IApp -) { - const mute = await Mute.find({ muterId: user._id }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); - - async function onNoteStream(noteId: any) { - const note = await packNote(noteId, user, { - detail: true - }); - - connection.send(JSON.stringify({ - type: 'note-updated', - body: { - note: note - } - })); - } - - // Subscribe Home stream channel - subscriber.on(`user-stream:${user._id}`, async x => { - //#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する - if (x.type == 'note') { - if (mutedUserIds.includes(x.body.userId)) { - return; - } - if (x.body.reply != null && mutedUserIds.includes(x.body.reply.userId)) { - return; - } - if (x.body.renote != null && mutedUserIds.includes(x.body.renote.userId)) { - return; - } - } else if (x.type == 'notification') { - if (mutedUserIds.includes(x.body.userId)) { - return; - } - } - //#endregion - - // Renoteなら再pack - if (x.type == 'note' && x.body.renoteId != null) { - x.body.renote = await pack(x.body.renoteId, user, { - detail: true - }); - } - - connection.send(JSON.stringify(x)); - }); - - connection.on('message', async data => { - const msg = JSON.parse(data.utf8Data); - - switch (msg.type) { - case 'api': - // 新鮮なデータを利用するためにユーザーをフェッチ - call(msg.endpoint, await User.findOne({ _id: user._id }), app, msg.data).then(res => { - connection.send(JSON.stringify({ - type: `api-res:${msg.id}`, - body: { res } - })); - }).catch(e => { - connection.send(JSON.stringify({ - type: `api-res:${msg.id}`, - body: { e } - })); - }); - break; - - case 'alive': - // Update lastUsedAt - User.update({ _id: user._id }, { - $set: { - 'lastUsedAt': new Date() - } - }); - break; - - case 'read_notification': - if (!msg.id) return; - readNotification(user._id, msg.id); - break; - - case 'capture': - if (!msg.id) return; - log(`CAPTURE: ${msg.id} by @${user.username}`); - subscriber.on(`note-stream:${msg.id}`, onNoteStream); - break; - - case 'decapture': - if (!msg.id) return; - log(`DECAPTURE: ${msg.id} by @${user.username}`); - subscriber.off(`note-stream:${msg.id}`, onNoteStream); - break; - } - }); -} diff --git a/src/server/api/stream/hybrid-timeline.ts b/src/server/api/stream/hybrid-timeline.ts deleted file mode 100644 index c401145abe..0000000000 --- a/src/server/api/stream/hybrid-timeline.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; - -import { IUser } from '../../../models/user'; -import Mute from '../../../models/mute'; -import { pack } from '../../../models/note'; - -export default async function( - request: websocket.request, - connection: websocket.connection, - subscriber: Xev, - user: IUser -) { - const mute = await Mute.find({ muterId: user._id }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); - - // Subscribe stream - subscriber.on('hybrid-timeline', onEvent); - subscriber.on(`hybrid-timeline:${user._id}`, onEvent); - - async function onEvent(note: any) { - //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (mutedUserIds.indexOf(note.userId) != -1) { - return; - } - if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) { - return; - } - if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) { - return; - } - //#endregion - - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, user, { - detail: true - }); - } - - connection.send(JSON.stringify({ - type: 'note', - body: note - })); - } -} diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts new file mode 100644 index 0000000000..ef6397fcd9 --- /dev/null +++ b/src/server/api/stream/index.ts @@ -0,0 +1,220 @@ +import autobind from 'autobind-decorator'; +import * as websocket from 'websocket'; +import Xev from 'xev'; +import * as debug from 'debug'; + +import User, { IUser } from '../../../models/user'; +import readNotification from '../common/read-notification'; +import call from '../call'; +import { IApp } from '../../../models/app'; +import readNote from '../../../services/note/read'; + +import Channel from './channel'; +import channels from './channels'; + +const log = debug('misskey'); + +/** + * Main stream connection + */ +export default class Connection { + public user?: IUser; + public app: IApp; + private wsConnection: websocket.connection; + public subscriber: Xev; + private channels: Channel[] = []; + private subscribingNotes: any = {}; + public sendMessageToWsOverride: any = null; // 後方互換性のため + + constructor( + wsConnection: websocket.connection, + subscriber: Xev, + user: IUser, + app: IApp + ) { + this.wsConnection = wsConnection; + this.user = user; + this.app = app; + this.subscriber = subscriber; + + this.wsConnection.on('message', this.onWsConnectionMessage); + } + + /** + * クライアントからメッセージ受信時 + */ + @autobind + private async onWsConnectionMessage(data: websocket.IMessage) { + const { type, body } = JSON.parse(data.utf8Data); + + switch (type) { + case 'api': this.onApiRequest(body); break; + case 'alive': this.onAlive(); break; + case 'readNotification': this.onReadNotification(body); break; + case 'subNote': this.onSubscribeNote(body); break; + case 'sn': this.onSubscribeNote(body); break; // alias + case 'unsubNote': this.onUnsubscribeNote(body); break; + case 'un': this.onUnsubscribeNote(body); break; // alias + case 'connect': this.onChannelConnectRequested(body); break; + case 'disconnect': this.onChannelDisconnectRequested(body); break; + case 'channel': this.onChannelMessageRequested(body); break; + } + } + + /** + * APIリクエスト要求時 + */ + @autobind + private async onApiRequest(payload: any) { + // 新鮮なデータを利用するためにユーザーをフェッチ + const user = this.user ? await User.findOne({ _id: this.user._id }) : null; + + const endpoint = payload.endpoint || payload.ep; // alias + + // 呼び出し + call(endpoint, user, this.app, payload.data).then(res => { + this.sendMessageToWs(`api:${payload.id}`, { res }); + }).catch(e => { + this.sendMessageToWs(`api:${payload.id}`, { e }); + }); + } + + @autobind + private onAlive() { + // Update lastUsedAt + User.update({ _id: this.user._id }, { + $set: { + 'lastUsedAt': new Date() + } + }); + } + + @autobind + private onReadNotification(payload: any) { + if (!payload.id) return; + readNotification(this.user._id, payload.id); + } + + /** + * 投稿購読要求時 + */ + @autobind + private onSubscribeNote(payload: any) { + if (!payload.id) return; + + if (this.subscribingNotes[payload.id] == null) { + this.subscribingNotes[payload.id] = 0; + } + + this.subscribingNotes[payload.id]++; + + if (this.subscribingNotes[payload.id] == 1) { + this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); + } + + if (payload.read) { + readNote(this.user._id, payload.id); + } + } + + /** + * 投稿購読解除要求時 + */ + @autobind + private onUnsubscribeNote(payload: any) { + if (!payload.id) return; + + this.subscribingNotes[payload.id]--; + if (this.subscribingNotes[payload.id] <= 0) { + delete this.subscribingNotes[payload.id]; + this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage); + } + } + + @autobind + private async onNoteStreamMessage(data: any) { + this.sendMessageToWs('noteUpdated', { + id: data.body.id, + type: data.type, + body: data.body.body, + }); + } + + /** + * チャンネル接続要求時 + */ + @autobind + private onChannelConnectRequested(payload: any) { + const { channel, id, params } = payload; + log(`CH CONNECT: ${id} ${channel} by @${this.user.username}`); + this.connectChannel(id, params, (channels as any)[channel]); + } + + /** + * チャンネル切断要求時 + */ + @autobind + private onChannelDisconnectRequested(payload: any) { + const { id } = payload; + log(`CH DISCONNECT: ${id} by @${this.user.username}`); + this.disconnectChannel(id); + } + + /** + * クライアントにメッセージ送信 + */ + @autobind + public sendMessageToWs(type: string, payload: any) { + if (this.sendMessageToWsOverride) return this.sendMessageToWsOverride(type, payload); // 後方互換性のため + this.wsConnection.send(JSON.stringify({ + type: type, + body: payload + })); + } + + /** + * チャンネルに接続 + */ + @autobind + public connectChannel(id: string, params: any, channelClass: { new(id: string, connection: Connection): Channel }) { + const channel = new channelClass(id, this); + this.channels.push(channel); + channel.init(params); + } + + /** + * チャンネルから切断 + * @param id チャンネルコネクションID + */ + @autobind + public disconnectChannel(id: string) { + const channel = this.channels.find(c => c.id === id); + + if (channel) { + if (channel.dispose) channel.dispose(); + this.channels = this.channels.filter(c => c.id !== id); + } + } + + /** + * チャンネルへメッセージ送信要求時 + * @param data メッセージ + */ + @autobind + private onChannelMessageRequested(data: any) { + const channel = this.channels.find(c => c.id === data.id); + if (channel != null && channel.onMessage != null) { + channel.onMessage(data.type, data.body); + } + } + + /** + * ストリームが切れたとき + */ + @autobind + public dispose() { + this.channels.forEach(c => { + if (c.dispose) c.dispose(); + }); + } +} diff --git a/src/server/api/stream/local-timeline.ts b/src/server/api/stream/local-timeline.ts deleted file mode 100644 index 82060a7aaa..0000000000 --- a/src/server/api/stream/local-timeline.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; - -import { IUser } from '../../../models/user'; -import Mute from '../../../models/mute'; -import { pack } from '../../../models/note'; - -export default async function( - request: websocket.request, - connection: websocket.connection, - subscriber: Xev, - user: IUser -) { - const mute = await Mute.find({ muterId: user._id }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); - - // Subscribe stream - subscriber.on('local-timeline', async note => { - //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (mutedUserIds.indexOf(note.userId) != -1) { - return; - } - if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) { - return; - } - if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) { - return; - } - //#endregion - - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, user, { - detail: true - }); - } - - connection.send(JSON.stringify({ - type: 'note', - body: note - })); - }); -} diff --git a/src/server/api/stream/messaging-index.ts b/src/server/api/stream/messaging-index.ts deleted file mode 100644 index 9af63f2812..0000000000 --- a/src/server/api/stream/messaging-index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; - -export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void { - // Subscribe messaging index stream - subscriber.on(`messaging-index-stream:${user._id}`, data => { - connection.send(JSON.stringify(data)); - }); -} diff --git a/src/server/api/stream/messaging.ts b/src/server/api/stream/messaging.ts deleted file mode 100644 index 8b352cea3c..0000000000 --- a/src/server/api/stream/messaging.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; -import read from '../common/read-messaging-message'; -import { ParsedUrlQuery } from 'querystring'; - -export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void { - const q = request.resourceURL.query as ParsedUrlQuery; - const otherparty = q.otherparty as string; - - // Subscribe messaging stream - subscriber.on(`messaging-stream:${user._id}-${otherparty}`, data => { - connection.send(JSON.stringify(data)); - }); - - connection.on('message', async (data) => { - const msg = JSON.parse(data.utf8Data); - - switch (msg.type) { - case 'read': - if (!msg.id) return; - read(user._id, otherparty, msg.id); - break; - } - }); -} diff --git a/src/server/api/stream/notes-stats.ts b/src/server/api/stream/notes-stats.ts deleted file mode 100644 index ab00620018..0000000000 --- a/src/server/api/stream/notes-stats.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; - -const ev = new Xev(); - -export default function(request: websocket.request, connection: websocket.connection): void { - const onStats = (stats: any) => { - connection.send(JSON.stringify({ - type: 'stats', - body: stats - })); - }; - - connection.on('message', async data => { - const msg = JSON.parse(data.utf8Data); - - switch (msg.type) { - case 'requestLog': - ev.once('notesStatsLog:' + msg.id, statsLog => { - connection.send(JSON.stringify({ - type: 'statsLog', - body: statsLog - })); - }); - ev.emit('requestNotesStatsLog', msg.id); - break; - } - }); - - ev.addListener('notesStats', onStats); - - connection.on('close', () => { - ev.removeListener('notesStats', onStats); - }); -} diff --git a/src/server/api/stream/server-stats.ts b/src/server/api/stream/server-stats.ts deleted file mode 100644 index f6c1f14ebe..0000000000 --- a/src/server/api/stream/server-stats.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; - -const ev = new Xev(); - -export default function(request: websocket.request, connection: websocket.connection): void { - const onStats = (stats: any) => { - connection.send(JSON.stringify({ - type: 'stats', - body: stats - })); - }; - - connection.on('message', async data => { - const msg = JSON.parse(data.utf8Data); - - switch (msg.type) { - case 'requestLog': - ev.once('serverStatsLog:' + msg.id, statsLog => { - connection.send(JSON.stringify({ - type: 'statsLog', - body: statsLog - })); - }); - ev.emit('requestServerStatsLog', { - id: msg.id, - length: msg.length - }); - break; - } - }); - - ev.addListener('serverStats', onStats); - - connection.on('close', () => { - ev.removeListener('serverStats', onStats); - }); -} diff --git a/src/server/api/stream/user-list.ts b/src/server/api/stream/user-list.ts deleted file mode 100644 index 30f94d5251..0000000000 --- a/src/server/api/stream/user-list.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as websocket from 'websocket'; -import Xev from 'xev'; -import { ParsedUrlQuery } from 'querystring'; - -export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void { - const q = request.resourceURL.query as ParsedUrlQuery; - const listId = q.listId as string; - - // Subscribe stream - subscriber.on(`user-list-stream:${listId}`, data => { - connection.send(JSON.stringify(data)); - }); -} diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index c8b2d4e0b9..c8c4a8a294 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -2,25 +2,13 @@ import * as http from 'http'; import * as websocket from 'websocket'; import Xev from 'xev'; -import homeStream from './stream/home'; -import localTimelineStream from './stream/local-timeline'; -import hybridTimelineStream from './stream/hybrid-timeline'; -import globalTimelineStream from './stream/global-timeline'; -import userListStream from './stream/user-list'; -import driveStream from './stream/drive'; -import messagingStream from './stream/messaging'; -import messagingIndexStream from './stream/messaging-index'; -import reversiGameStream from './stream/games/reversi-game'; -import reversiStream from './stream/games/reversi'; -import serverStatsStream from './stream/server-stats'; -import notesStatsStream from './stream/notes-stats'; +import MainStreamConnection from './stream'; import { ParsedUrlQuery } from 'querystring'; import authenticate from './authenticate'; +import channels from './stream/channels'; module.exports = (server: http.Server) => { - /** - * Init websocket server - */ + // Init websocket server const ws = new websocket.server({ httpServer: server }); @@ -28,52 +16,45 @@ module.exports = (server: http.Server) => { ws.on('request', async (request) => { const connection = request.accept(); - if (request.resourceURL.pathname === '/server-stats') { - serverStatsStream(request, connection); - return; - } - - if (request.resourceURL.pathname === '/notes-stats') { - notesStatsStream(request, connection); - return; - } - const ev = new Xev(); - connection.once('close', () => { - ev.removeAllListeners(); - }); - const q = request.resourceURL.query as ParsedUrlQuery; const [user, app] = await authenticate(q.i as string); - if (request.resourceURL.pathname === '/games/reversi-game') { - reversiGameStream(request, connection, ev, user); - return; - } + const main = new MainStreamConnection(connection, ev, user, app); + + // 後方互換性のため + if (request.resourceURL.pathname !== '/streaming') { + main.sendMessageToWsOverride = (type: string, payload: any) => { + if (type == 'channel') { + type = payload.type; + payload = payload.body; + } + if (type.startsWith('api:')) { + type = type.replace('api:', 'api-res:'); + } + connection.send(JSON.stringify({ + type: type, + body: payload + })); + }; - if (user == null) { - connection.send('authentication-failed'); - connection.close(); - return; + main.connectChannel(Math.random().toString(), null, + request.resourceURL.pathname === '/' ? channels.homeTimeline : + request.resourceURL.pathname === '/local-timeline' ? channels.localTimeline : + request.resourceURL.pathname === '/hybrid-timeline' ? channels.hybridTimeline : + request.resourceURL.pathname === '/global-timeline' ? channels.globalTimeline : null); } - const channel: any = - request.resourceURL.pathname === '/' ? homeStream : - request.resourceURL.pathname === '/local-timeline' ? localTimelineStream : - request.resourceURL.pathname === '/hybrid-timeline' ? hybridTimelineStream : - request.resourceURL.pathname === '/global-timeline' ? globalTimelineStream : - request.resourceURL.pathname === '/user-list' ? userListStream : - request.resourceURL.pathname === '/drive' ? driveStream : - request.resourceURL.pathname === '/messaging' ? messagingStream : - request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream : - request.resourceURL.pathname === '/games/reversi' ? reversiStream : - null; + connection.once('close', () => { + ev.removeAllListeners(); + main.dispose(); + }); - if (channel !== null) { - channel(request, connection, ev, user, app); - } else { - connection.close(); - } + connection.on('message', async (data) => { + if (data.utf8Data == 'ping') { + connection.send('pong'); + } + }); }); }; |