diff options
Diffstat (limited to 'src/server/api/endpoints')
70 files changed, 1594 insertions, 409 deletions
diff --git a/src/server/api/endpoints/admin/abuse-user-reports.ts b/src/server/api/endpoints/admin/abuse-user-reports.ts new file mode 100644 index 0000000000..c88174f13f --- /dev/null +++ b/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -0,0 +1,54 @@ +import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; +import Report, { packMany } from '../../../../models/abuse-user-report'; +import define from '../../define'; + +export const meta = { + requireCredential: true, + requireModerator: true, + + params: { + limit: { + validator: $.num.optional.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.type(ID).optional, + transform: transform, + }, + + untilId: { + validator: $.type(ID).optional, + transform: transform, + }, + } +}; + +export default define(meta, (ps) => new Promise(async (res, rej) => { + if (ps.sinceId && ps.untilId) { + return rej('cannot set sinceId and untilId'); + } + + const sort = { + _id: -1 + }; + const query = {} as any; + if (ps.sinceId) { + sort._id = 1; + query._id = { + $gt: ps.sinceId + }; + } else if (ps.untilId) { + query._id = { + $lt: ps.untilId + }; + } + + const reports = await Report + .find(query, { + limit: ps.limit, + sort: sort + }); + + res(await packMany(reports)); +})); diff --git a/src/server/api/endpoints/admin/drive/files.ts b/src/server/api/endpoints/admin/drive/files.ts new file mode 100644 index 0000000000..177a808cbf --- /dev/null +++ b/src/server/api/endpoints/admin/drive/files.ts @@ -0,0 +1,81 @@ +import $ from 'cafy'; +import File, { packMany } from '../../../../../models/drive-file'; +import define from '../../../define'; + +export const meta = { + requireCredential: false, + requireModerator: true, + + params: { + limit: { + validator: $.num.optional.range(1, 100), + default: 10 + }, + + offset: { + validator: $.num.optional.min(0), + default: 0 + }, + + sort: { + validator: $.str.optional.or([ + '+createdAt', + '-createdAt', + '+size', + '-size', + ]), + }, + + origin: { + validator: $.str.optional.or([ + 'combined', + 'local', + 'remote', + ]), + default: 'local' + } + } +}; + +export default define(meta, (ps, me) => new Promise(async (res, rej) => { + let _sort; + if (ps.sort) { + if (ps.sort == '+createdAt') { + _sort = { + uploadDate: -1 + }; + } else if (ps.sort == '-createdAt') { + _sort = { + uploadDate: 1 + }; + } else if (ps.sort == '+size') { + _sort = { + length: -1 + }; + } else if (ps.sort == '-size') { + _sort = { + length: 1 + }; + } + } else { + _sort = { + _id: -1 + }; + } + + const q = { + 'metadata.deletedAt': { $exists: false }, + } as any; + + if (ps.origin == 'local') q['metadata._user.host'] = null; + if (ps.origin == 'remote') q['metadata._user.host'] = { $ne: null }; + + const files = await File + .find(q, { + limit: ps.limit, + sort: _sort, + skip: ps.offset + }); + + res(await packMany(files, { detail: true, withUser: true, self: true })); +})); diff --git a/src/server/api/endpoints/admin/drive/show-file.ts b/src/server/api/endpoints/admin/drive/show-file.ts new file mode 100644 index 0000000000..6dfab19643 --- /dev/null +++ b/src/server/api/endpoints/admin/drive/show-file.ts @@ -0,0 +1,28 @@ +import $ from 'cafy'; +import ID, { transform } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import DriveFile from '../../../../../models/drive-file'; + +export const meta = { + requireCredential: true, + requireModerator: true, + + params: { + fileId: { + validator: $.type(ID), + transform: transform, + }, + } +}; + +export default define(meta, (ps, me) => new Promise(async (res, rej) => { + const file = await DriveFile.findOne({ + _id: ps.fileId + }); + + if (file == null) { + return rej('file not found'); + } + + res(file); +})); diff --git a/src/server/api/endpoints/admin/remove-abuse-user-report.ts b/src/server/api/endpoints/admin/remove-abuse-user-report.ts new file mode 100644 index 0000000000..4d068a410e --- /dev/null +++ b/src/server/api/endpoints/admin/remove-abuse-user-report.ts @@ -0,0 +1,32 @@ +import $ from 'cafy'; +import ID, { transform } from '../../../../misc/cafy-id'; +import define from '../../define'; +import AbuseUserReport from '../../../../models/abuse-user-report'; + +export const meta = { + requireCredential: true, + requireModerator: true, + + params: { + reportId: { + validator: $.type(ID), + transform: transform + }, + } +}; + +export default define(meta, (ps) => new Promise(async (res, rej) => { + const report = await AbuseUserReport.findOne({ + _id: ps.reportId + }); + + if (report == null) { + return rej('report not found'); + } + + await AbuseUserReport.remove({ + _id: report._id + }); + + res(); +})); diff --git a/src/server/api/endpoints/admin/reset-password.ts b/src/server/api/endpoints/admin/reset-password.ts new file mode 100644 index 0000000000..c072c12e0d --- /dev/null +++ b/src/server/api/endpoints/admin/reset-password.ts @@ -0,0 +1,57 @@ +import $ from 'cafy'; +import ID, { transform } from '../../../../misc/cafy-id'; +import define from '../../define'; +import User from '../../../../models/user'; +import * as bcrypt from 'bcryptjs'; +import rndstr from 'rndstr'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーのパスワードをリセットします。', + }, + + requireCredential: true, + requireModerator: true, + + params: { + userId: { + validator: $.type(ID), + transform: transform, + desc: { + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to suspend' + } + }, + } +}; + +export default define(meta, (ps) => new Promise(async (res, rej) => { + const user = await User.findOne({ + _id: ps.userId + }); + + if (user == null) { + return rej('user not found'); + } + + if (user.isAdmin) { + return rej('cannot reset password of admin'); + } + + const passwd = rndstr('a-zA-Z0-9', 8); + + // Generate hash of password + const hash = bcrypt.hashSync(passwd); + + await User.findOneAndUpdate({ + _id: user._id + }, { + $set: { + password: hash + } + }); + + res({ + password: passwd + }); +})); diff --git a/src/server/api/endpoints/admin/show-user.ts b/src/server/api/endpoints/admin/show-user.ts new file mode 100644 index 0000000000..490b685352 --- /dev/null +++ b/src/server/api/endpoints/admin/show-user.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import ID, { transform } from '../../../../misc/cafy-id'; +import define from '../../define'; +import User from '../../../../models/user'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーの情報を取得します。', + }, + + requireCredential: true, + requireModerator: true, + + params: { + userId: { + validator: $.type(ID), + transform: transform, + desc: { + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to suspend' + } + }, + } +}; + +export default define(meta, (ps, me) => new Promise(async (res, rej) => { + const user = await User.findOne({ + _id: ps.userId + }); + + if (user == null) { + return rej('user not found'); + } + + if (me.isModerator && user.isAdmin) { + return rej('cannot show info of admin'); + } + + res(user); +})); diff --git a/src/server/api/endpoints/admin/show-users.ts b/src/server/api/endpoints/admin/show-users.ts new file mode 100644 index 0000000000..20ccfbd7f3 --- /dev/null +++ b/src/server/api/endpoints/admin/show-users.ts @@ -0,0 +1,123 @@ +import $ from 'cafy'; +import User, { pack } from '../../../../models/user'; +import define from '../../define'; + +export const meta = { + requireCredential: true, + requireModerator: true, + + params: { + limit: { + validator: $.num.optional.range(1, 100), + default: 10 + }, + + offset: { + validator: $.num.optional.min(0), + default: 0 + }, + + sort: { + validator: $.str.optional.or([ + '+follower', + '-follower', + '+createdAt', + '-createdAt', + '+updatedAt', + '-updatedAt', + ]), + }, + + state: { + validator: $.str.optional.or([ + 'all', + 'admin', + 'moderator', + 'adminOrModerator', + 'verified', + 'suspended', + ]), + default: 'all' + }, + + origin: { + validator: $.str.optional.or([ + 'combined', + 'local', + 'remote', + ]), + default: 'local' + } + } +}; + +export default define(meta, (ps, me) => new Promise(async (res, rej) => { + let _sort; + if (ps.sort) { + if (ps.sort == '+follower') { + _sort = { + followersCount: -1 + }; + } else if (ps.sort == '-follower') { + _sort = { + followersCount: 1 + }; + } else if (ps.sort == '+createdAt') { + _sort = { + createdAt: -1 + }; + } else if (ps.sort == '+updatedAt') { + _sort = { + updatedAt: -1 + }; + } else if (ps.sort == '-createdAt') { + _sort = { + createdAt: 1 + }; + } else if (ps.sort == '-updatedAt') { + _sort = { + updatedAt: 1 + }; + } + } else { + _sort = { + _id: -1 + }; + } + + const q = { + $and: [] + } as any; + + // state + q.$and.push( + ps.state == 'admin' ? { isAdmin: true } : + ps.state == 'moderator' ? { isModerator: true } : + ps.state == 'adminOrModerator' ? { + $or: [{ + isAdmin: true + }, { + isModerator: true + }] + } : + ps.state == 'verified' ? { isVerified: true } : + ps.state == 'suspended' ? { isSuspended: true } : + {} + ); + + // origin + q.$and.push( + ps.origin == 'local' ? { host: null } : + ps.origin == 'remote' ? { host: { $ne: null } } : + {} + ); + + const users = await User + .find(q, { + limit: ps.limit, + sort: _sort, + skip: ps.offset + }); + + res(await Promise.all(users.map(user => pack(user, me, { detail: true })))); +})); diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts index 5bbd387a20..2ec5196880 100644 --- a/src/server/api/endpoints/admin/suspend-user.ts +++ b/src/server/api/endpoints/admin/suspend-user.ts @@ -37,6 +37,10 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { return rej('cannot suspend admin'); } + if (user.isModerator) { + return rej('cannot suspend moderator'); + } + await User.findOneAndUpdate({ _id: user._id }, { diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index bbae212bd7..13663243a2 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -32,6 +32,13 @@ export const meta = { } }, + disableGlobalTimeline: { + validator: $.bool.optional.nullable, + desc: { + 'ja-JP': 'グローバルタイムラインを無効にするか否か' + } + }, + hidedTags: { validator: $.arr($.str).optional.nullable, desc: { @@ -39,6 +46,13 @@ export const meta = { } }, + mascotImageUrl: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'インスタンスキャラクター画像のURL' + } + }, + bannerUrl: { validator: $.str.optional.nullable, desc: { @@ -46,6 +60,13 @@ export const meta = { } }, + errorImageUrl: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'インスタンスのエラー画像URL' + } + }, + name: { validator: $.str.optional.nullable, desc: { @@ -139,6 +160,13 @@ export const meta = { } }, + summalyProxy: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'summalyプロキシURL' + } + }, + enableTwitterIntegration: { validator: $.bool.optional, desc: { @@ -200,7 +228,98 @@ export const meta = { desc: { 'ja-JP': 'DiscordアプリのClient Secret' } - } + }, + + enableExternalUserRecommendation: { + validator: $.bool.optional, + desc: { + 'ja-JP': '外部ユーザーレコメンデーションを有効にする' + } + }, + + externalUserRecommendationEngine: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': '外部ユーザーレコメンデーションのサードパーティエンジン' + } + }, + + externalUserRecommendationTimeout: { + validator: $.num.optional.nullable.min(0), + desc: { + 'ja-JP': '外部ユーザーレコメンデーションのタイムアウト (ミリ秒)' + } + }, + + enableEmail: { + validator: $.bool.optional, + desc: { + 'ja-JP': 'メール配信を有効にするか否か' + } + }, + + email: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'メール配信する際に利用するメールアドレス' + } + }, + + smtpSecure: { + validator: $.bool.optional, + desc: { + 'ja-JP': 'SMTPサーバがSSLを使用しているか否か' + } + }, + + smtpHost: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'SMTPサーバのホスト' + } + }, + + smtpPort: { + validator: $.num.optional.nullable, + desc: { + 'ja-JP': 'SMTPサーバのポート' + } + }, + + smtpUser: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'SMTPサーバのユーザー名' + } + }, + + smtpPass: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'SMTPサーバのパスワード' + } + }, + + enableServiceWorker: { + validator: $.bool.optional, + desc: { + 'ja-JP': 'ServiceWorkerを有効にするか否か' + } + }, + + swPublicKey: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'ServiceWorkerのVAPIDキーペアの公開鍵' + } + }, + + swPrivateKey: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'ServiceWorkerのVAPIDキーペアの秘密鍵' + } + }, } }; @@ -219,10 +338,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { set.disableLocalTimeline = ps.disableLocalTimeline; } + if (typeof ps.disableGlobalTimeline === 'boolean') { + set.disableGlobalTimeline = ps.disableGlobalTimeline; + } + if (Array.isArray(ps.hidedTags)) { set.hidedTags = ps.hidedTags; } + if (ps.mascotImageUrl !== undefined) { + set.mascotImageUrl = ps.mascotImageUrl; + } + if (ps.bannerUrl !== undefined) { set.bannerUrl = ps.bannerUrl; } @@ -279,6 +406,10 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { set.langs = ps.langs; } + if (ps.summalyProxy !== undefined) { + set.summalyProxy = ps.summalyProxy; + } + if (ps.enableTwitterIntegration !== undefined) { set.enableTwitterIntegration = ps.enableTwitterIntegration; } @@ -315,6 +446,62 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { set.discordClientSecret = ps.discordClientSecret; } + if (ps.enableExternalUserRecommendation !== undefined) { + set.enableExternalUserRecommendation = ps.enableExternalUserRecommendation; + } + + if (ps.externalUserRecommendationEngine !== undefined) { + set.externalUserRecommendationEngine = ps.externalUserRecommendationEngine; + } + + if (ps.externalUserRecommendationTimeout !== undefined) { + set.externalUserRecommendationTimeout = ps.externalUserRecommendationTimeout; + } + + if (ps.enableEmail !== undefined) { + set.enableEmail = ps.enableEmail; + } + + if (ps.email !== undefined) { + set.email = ps.email; + } + + if (ps.smtpSecure !== undefined) { + set.smtpSecure = ps.smtpSecure; + } + + if (ps.smtpHost !== undefined) { + set.smtpHost = ps.smtpHost; + } + + if (ps.smtpPort !== undefined) { + set.smtpPort = ps.smtpPort; + } + + if (ps.smtpUser !== undefined) { + set.smtpUser = ps.smtpUser; + } + + if (ps.smtpPass !== undefined) { + set.smtpPass = ps.smtpPass; + } + + if (ps.errorImageUrl !== undefined) { + set.errorImageUrl = ps.errorImageUrl; + } + + if (ps.enableServiceWorker !== undefined) { + set.enableServiceWorker = ps.enableServiceWorker; + } + + if (ps.swPublicKey !== undefined) { + set.swPublicKey = ps.swPublicKey; + } + + if (ps.swPrivateKey !== undefined) { + set.swPrivateKey = ps.swPrivateKey; + } + 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 index f8fc7162f5..7577711b5f 100644 --- a/src/server/api/endpoints/aggregation/hashtags.ts +++ b/src/server/api/endpoints/aggregation/hashtags.ts @@ -47,10 +47,7 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { }> = []; // カウント - data.map(x => x._id).forEach(x => { - // ブラックリストに登録されているタグなら弾く - if (hidedTags.includes(x.tag)) return; - + for (const x of data.map(x => x._id).filter(x => !hidedTags.includes(x.tag))) { const i = tags.findIndex(tag => tag.name == x.tag); if (i != -1) { tags[i].count++; @@ -60,10 +57,10 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { count: 1 }); } - }); + } // タグを人気順に並べ替え - tags = tags.sort((a, b) => b.count - a.count); + tags.sort((a, b) => b.count - a.count); tags = tags.slice(0, 30); diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts index 778d1e5099..3b4021e0a7 100644 --- a/src/server/api/endpoints/ap/show.ts +++ b/src/server/api/endpoints/ap/show.ts @@ -25,11 +25,10 @@ export const meta = { }, }; -export default define(meta, (ps) => new Promise(async (res, rej) => { - const object = await fetchAny(ps.uri); - if (object == null) return rej('object not found'); - - res(object); +export default define(meta, (ps) => new Promise((res, rej) => { + fetchAny(ps.uri) + .then(object => object != null ? res(object) : rej('object not found')) + .catch(e => rej(e)); })); /*** @@ -80,7 +79,7 @@ async function fetchAny(uri: string) { const user = await createPerson(object.id); return { type: 'User', - object: user + object: await packUser(user, null, { detail: true }) }; } @@ -88,7 +87,7 @@ async function fetchAny(uri: string) { const note = await createNote(object.id); return { type: 'Note', - object: note + object: await packNote(note, null, { detail: true }) }; } diff --git a/src/server/api/endpoints/charts/active-users.ts b/src/server/api/endpoints/charts/active-users.ts new file mode 100644 index 0000000000..5187e5b353 --- /dev/null +++ b/src/server/api/endpoints/charts/active-users.ts @@ -0,0 +1,34 @@ +import $ from 'cafy'; +import define from '../../define'; +import activeUsersChart from '../../../../chart/active-users'; + +export const meta = { + stability: 'stable', + + desc: { + 'ja-JP': 'アクティブユーザーのチャートを取得します。' + }, + + params: { + span: { + validator: $.str.or(['day', 'hour']), + desc: { + 'ja-JP': '集計のスパン (day または hour)' + } + }, + + limit: { + validator: $.num.optional.range(1, 500), + default: 30, + desc: { + 'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。' + } + }, + } +}; + +export default define(meta, (ps) => new Promise(async (res, rej) => { + const stats = await activeUsersChart.getChart(ps.span as any, ps.limit); + + res(stats); +})); diff --git a/src/server/api/endpoints/charts/drive.ts b/src/server/api/endpoints/charts/drive.ts index 25ede16102..2fd0139e8e 100644 --- a/src/server/api/endpoints/charts/drive.ts +++ b/src/server/api/endpoints/charts/drive.ts @@ -3,6 +3,8 @@ import define from '../../define'; import driveChart from '../../../../chart/drive'; export const meta = { + stability: 'stable', + desc: { 'ja-JP': 'ドライブのチャートを取得します。' }, diff --git a/src/server/api/endpoints/charts/federation.ts b/src/server/api/endpoints/charts/federation.ts index fb5fa4426e..f7d12e5c33 100644 --- a/src/server/api/endpoints/charts/federation.ts +++ b/src/server/api/endpoints/charts/federation.ts @@ -3,6 +3,8 @@ import define from '../../define'; import federationChart from '../../../../chart/federation'; export const meta = { + stability: 'stable', + desc: { 'ja-JP': 'フェデレーションのチャートを取得します。' }, diff --git a/src/server/api/endpoints/charts/hashtag.ts b/src/server/api/endpoints/charts/hashtag.ts index b2140068a5..3df2dd8500 100644 --- a/src/server/api/endpoints/charts/hashtag.ts +++ b/src/server/api/endpoints/charts/hashtag.ts @@ -3,6 +3,8 @@ import define from '../../define'; import hashtagChart from '../../../../chart/hashtag'; export const meta = { + stability: 'stable', + desc: { 'ja-JP': 'ハッシュタグごとのチャートを取得します。' }, diff --git a/src/server/api/endpoints/charts/network.ts b/src/server/api/endpoints/charts/network.ts index 6d7468f950..af5b799a80 100644 --- a/src/server/api/endpoints/charts/network.ts +++ b/src/server/api/endpoints/charts/network.ts @@ -3,6 +3,8 @@ import define from '../../define'; import networkChart from '../../../../chart/network'; export const meta = { + stability: 'stable', + desc: { 'ja-JP': 'ネットワークのチャートを取得します。' }, diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts index c9889d1840..906cef1fa9 100644 --- a/src/server/api/endpoints/charts/notes.ts +++ b/src/server/api/endpoints/charts/notes.ts @@ -3,6 +3,8 @@ import define from '../../define'; import notesChart from '../../../../chart/notes'; export const meta = { + stability: 'stable', + desc: { 'ja-JP': '投稿のチャートを取得します。' }, diff --git a/src/server/api/endpoints/charts/user/drive.ts b/src/server/api/endpoints/charts/user/drive.ts index 7e806c151b..1ef133c6bf 100644 --- a/src/server/api/endpoints/charts/user/drive.ts +++ b/src/server/api/endpoints/charts/user/drive.ts @@ -4,6 +4,8 @@ import perUserDriveChart from '../../../../../chart/per-user-drive'; import ID, { transform } from '../../../../../misc/cafy-id'; export const meta = { + stability: 'stable', + desc: { 'ja-JP': 'ユーザーごとのドライブのチャートを取得します。' }, diff --git a/src/server/api/endpoints/charts/user/following.ts b/src/server/api/endpoints/charts/user/following.ts index ee20b3c39c..c8a64455e5 100644 --- a/src/server/api/endpoints/charts/user/following.ts +++ b/src/server/api/endpoints/charts/user/following.ts @@ -4,6 +4,8 @@ import perUserFollowingChart from '../../../../../chart/per-user-following'; import ID, { transform } from '../../../../../misc/cafy-id'; export const meta = { + stability: 'stable', + desc: { 'ja-JP': 'ユーザーごとのフォロー/フォロワーのチャートを取得します。' }, diff --git a/src/server/api/endpoints/charts/user/notes.ts b/src/server/api/endpoints/charts/user/notes.ts index 1b6a312a01..f8a3c726ef 100644 --- a/src/server/api/endpoints/charts/user/notes.ts +++ b/src/server/api/endpoints/charts/user/notes.ts @@ -4,6 +4,8 @@ import perUserNotesChart from '../../../../../chart/per-user-notes'; import ID, { transform } from '../../../../../misc/cafy-id'; export const meta = { + stability: 'stable', + desc: { 'ja-JP': 'ユーザーごとの投稿のチャートを取得します。' }, diff --git a/src/server/api/endpoints/charts/user/reactions.ts b/src/server/api/endpoints/charts/user/reactions.ts index 78bd22cc2f..f24c84593c 100644 --- a/src/server/api/endpoints/charts/user/reactions.ts +++ b/src/server/api/endpoints/charts/user/reactions.ts @@ -4,6 +4,8 @@ import perUserReactionsChart from '../../../../../chart/per-user-reactions'; import ID, { transform } from '../../../../../misc/cafy-id'; export const meta = { + stability: 'stable', + desc: { 'ja-JP': 'ユーザーごとの被リアクション数のチャートを取得します。' }, diff --git a/src/server/api/endpoints/charts/users.ts b/src/server/api/endpoints/charts/users.ts index 733b7b8178..00c2871148 100644 --- a/src/server/api/endpoints/charts/users.ts +++ b/src/server/api/endpoints/charts/users.ts @@ -3,6 +3,8 @@ import define from '../../define'; import usersChart from '../../../../chart/users'; export const meta = { + stability: 'stable', + desc: { 'ja-JP': 'ユーザーのチャートを取得します。' }, diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts index 27f101562d..20955e0e4e 100644 --- a/src/server/api/endpoints/drive/files.ts +++ b/src/server/api/endpoints/drive/files.ts @@ -77,5 +77,5 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { sort: sort }); - res(await packMany(files)); + res(await packMany(files, { detail: false, self: true })); })); diff --git a/src/server/api/endpoints/drive/files/check_existence.ts b/src/server/api/endpoints/drive/files/check_existence.ts index d3ba4b386d..6e986d4170 100644 --- a/src/server/api/endpoints/drive/files/check_existence.ts +++ b/src/server/api/endpoints/drive/files/check_existence.ts @@ -32,6 +32,6 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { if (file === null) { res({ file: null }); } else { - res({ file: await pack(file) }); + res({ file: await pack(file, { self: true }) }); } })); diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index 53c62dd868..0660627f08 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -74,7 +74,7 @@ export default define(meta, (ps, user, app, file, cleanup) => new Promise(async cleanup(); - res(pack(driveFile)); + res(pack(driveFile, { self: true })); } catch (e) { console.error(e); diff --git a/src/server/api/endpoints/drive/files/delete.ts b/src/server/api/endpoints/drive/files/delete.ts index 7367c8fbb6..0c2799c708 100644 --- a/src/server/api/endpoints/drive/files/delete.ts +++ b/src/server/api/endpoints/drive/files/delete.ts @@ -32,14 +32,17 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { // Fetch file const file = await DriveFile .findOne({ - _id: ps.fileId, - 'metadata.userId': user._id + _id: ps.fileId }); if (file === null) { return rej('file-not-found'); } + if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) { + return rej('access denied'); + } + // Delete await del(file); diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts index 8bc392fefe..25135e83a2 100644 --- a/src/server/api/endpoints/drive/files/find.ts +++ b/src/server/api/endpoints/drive/files/find.ts @@ -31,5 +31,5 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { 'metadata.folderId': ps.folderId }); - res(await Promise.all(files.map(file => pack(file)))); + res(await Promise.all(files.map(file => pack(file, { self: true })))); })); diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts index 450a97065b..e6d85a5efb 100644 --- a/src/server/api/endpoints/drive/files/show.ts +++ b/src/server/api/endpoints/drive/files/show.ts @@ -1,6 +1,9 @@ -import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFile, { pack } from '../../../../../models/drive-file'; +import $ from 'cafy'; +import * as mongo from 'mongodb'; +import ID, { transform } from '../../../../../misc/cafy-id'; +import DriveFile, { pack, IDriveFile } from '../../../../../models/drive-file'; import define from '../../../define'; +import config from '../../../../../config'; export const meta = { stability: 'stable', @@ -16,24 +19,62 @@ export const meta = { params: { fileId: { - validator: $.type(ID), + validator: $.type(ID).optional, transform: transform, desc: { 'ja-JP': '対象のファイルID', 'en-US': 'Target file ID' } + }, + + url: { + validator: $.str.optional, + desc: { + 'ja-JP': '対象のファイルのURL', + 'en-US': 'Target file URL' + } } } }; export default define(meta, (ps, user) => new Promise(async (res, rej) => { - // Fetch file - const file = await DriveFile - .findOne({ + let file: IDriveFile; + + if (ps.fileId) { + file = await DriveFile.findOne({ _id: ps.fileId, - 'metadata.userId': user._id, 'metadata.deletedAt': { $exists: false } }); + } else if (ps.url) { + const isInternalStorageUrl = ps.url.startsWith(config.drive_url); + if (isInternalStorageUrl) { + // Extract file ID from url + // e.g. + // http://misskey.local/files/foo?original=bar --> foo + const fileId = new mongo.ObjectID(ps.url.replace(config.drive_url, '').replace(/\?(.*)$/, '').replace(/\//g, '')); + file = await DriveFile.findOne({ + _id: fileId, + 'metadata.deletedAt': { $exists: false } + }); + } else { + file = await DriveFile.findOne({ + $or: [{ + 'metadata.url': ps.url + }, { + 'metadata.webpublicUrl': ps.url + }, { + 'metadata.thumbnailUrl': ps.url + }], + 'metadata.deletedAt': { $exists: false } + }); + } + } else { + return rej('fileId or url required'); + } + + if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) { + return rej('access denied'); + } if (file === null) { return rej('file-not-found'); @@ -41,7 +82,8 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { // Serialize const _file = await pack(file, { - detail: true + detail: true, + self: true }); res(_file); diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts index 4efec3dc2a..a17ff2bf34 100644 --- a/src/server/api/endpoints/drive/files/update.ts +++ b/src/server/api/endpoints/drive/files/update.ts @@ -57,14 +57,17 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { // Fetch file const file = await DriveFile .findOne({ - _id: ps.fileId, - 'metadata.userId': user._id + _id: ps.fileId }); if (file === null) { return rej('file-not-found'); } + if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) { + return rej('access denied'); + } + if (ps.name) file.filename = ps.name; if (ps.isSensitive !== undefined) file.metadata.isSensitive = ps.isSensitive; @@ -100,18 +103,18 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { Note.find({ '_files._id': file._id }).then(notes => { - notes.forEach(note => { + for (const note of notes) { note._files[note._files.findIndex(f => f._id.equals(file._id))] = file; Note.update({ _id: note._id }, { $set: { _files: note._files } }); - }); + } }); // Serialize - const fileObj = await pack(file); + const fileObj = await pack(file, { self: true }); // Response res(fileObj); diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts index a8faab1d73..fc386e1638 100644 --- a/src/server/api/endpoints/drive/files/upload_from_url.ts +++ b/src/server/api/endpoints/drive/files/upload_from_url.ts @@ -26,7 +26,7 @@ export const meta = { folderId: { validator: $.type(ID).optional.nullable, - default: null as any as any, + default: null as any, transform: transform }, @@ -50,5 +50,5 @@ export const meta = { }; export default define(meta, (ps, user) => new Promise(async (res, rej) => { - res(pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force))); + res(pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true })); })); diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts index 804ecf50d9..c8342c66b5 100644 --- a/src/server/api/endpoints/drive/stream.ts +++ b/src/server/api/endpoints/drive/stream.ts @@ -65,5 +65,5 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { sort: sort }); - res(await packMany(files)); + res(await packMany(files, { self: true })); })); diff --git a/src/server/api/endpoints/following/stalk.ts b/src/server/api/endpoints/following/stalk.ts deleted file mode 100644 index 3a58e2192f..0000000000 --- a/src/server/api/endpoints/following/stalk.ts +++ /dev/null @@ -1,50 +0,0 @@ -import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; -import Following from '../../../../models/following'; -import define from '../../define'; - -export const meta = { - desc: { - 'ja-JP': '指定したユーザーをストーキングします。', - 'en-US': 'Stalk a user.' - }, - - requireCredential: true, - - kind: 'following-write', - - params: { - userId: { - validator: $.type(ID), - transform: transform, - desc: { - 'ja-JP': '対象のユーザーのID', - 'en-US': 'Target user ID' - } - } - } -}; - -export default define(meta, (ps, user) => new Promise(async (res, rej) => { - const follower = user; - - // Fetch following - const following = await Following.findOne({ - followerId: follower._id, - followeeId: ps.userId - }); - - if (following === null) { - return rej('following not found'); - } - - // Stalk - await Following.update({ _id: following._id }, { - $set: { - stalk: true - } - }); - - res(); - - // TODO: イベント -})); diff --git a/src/server/api/endpoints/following/unstalk.ts b/src/server/api/endpoints/following/unstalk.ts deleted file mode 100644 index ad07ec38ba..0000000000 --- a/src/server/api/endpoints/following/unstalk.ts +++ /dev/null @@ -1,50 +0,0 @@ -import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; -import Following from '../../../../models/following'; -import define from '../../define'; - -export const meta = { - desc: { - 'ja-JP': '指定したユーザーのストーキングをやめます。', - 'en-US': 'Unstalk a user.' - }, - - requireCredential: true, - - kind: 'following-write', - - params: { - userId: { - validator: $.type(ID), - transform: transform, - desc: { - 'ja-JP': '対象のユーザーのID', - 'en-US': 'Target user ID' - } - } - } -}; - -export default define(meta, (ps, user) => new Promise(async (res, rej) => { - const follower = user; - - // Fetch following - const following = await Following.findOne({ - followerId: follower._id, - followeeId: ps.userId - }); - - if (following === null) { - return rej('following not found'); - } - - // Stalk - await Following.update({ _id: following._id }, { - $set: { - stalk: false - } - }); - - res(); - - // TODO: イベント -})); diff --git a/src/server/api/endpoints/games/reversi/games/show.ts b/src/server/api/endpoints/games/reversi/games/show.ts index c747202354..0882456102 100644 --- a/src/server/api/endpoints/games/reversi/games/show.ts +++ b/src/server/api/endpoints/games/reversi/games/show.ts @@ -25,9 +25,8 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { loopedBoard: game.settings.loopedBoard }); - game.logs.forEach(log => { + for (const log of game.logs) o.put(log.color, log.pos); - }); const packed = await pack(game, user); diff --git a/src/server/api/endpoints/games/reversi/match.ts b/src/server/api/endpoints/games/reversi/match.ts index 43b6fc8eed..a3cc523a8c 100644 --- a/src/server/api/endpoints/games/reversi/match.ts +++ b/src/server/api/endpoints/games/reversi/match.ts @@ -66,7 +66,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { }); if (other == 0) { - publishMainStream(user._id, 'reversi_no_invites'); + publishMainStream(user._id, 'reversiNoInvites'); } } else { // Fetch child diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts index ed4c8e337f..a26dbf0941 100644 --- a/src/server/api/endpoints/hashtags/trend.ts +++ b/src/server/api/endpoints/hashtags/trend.ts @@ -58,10 +58,7 @@ export default define(meta, () => new Promise(async (res, rej) => { }> = []; // カウント - data.map(x => x._id).forEach(x => { - // ブラックリストに登録されているタグなら弾く - if (hidedTags.includes(x.tag)) return; - + for (const x of data.map(x => x._id).filter(x => !hidedTags.includes(x.tag))) { const i = tags.findIndex(tag => tag.name == x.tag); if (i != -1) { tags[i].count++; @@ -71,7 +68,7 @@ export default define(meta, () => new Promise(async (res, rej) => { count: 1 }); } - }); + } // 最低要求投稿者数を下回るならカットする const limitedTags = tags.filter(tag => tag.count >= requiredUsers); diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts index aea47ad795..ab85b17d09 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -1,4 +1,4 @@ -import User, { pack } from '../../../models/user'; +import { pack } from '../../../models/user'; import define from '../define'; export const meta = { @@ -27,11 +27,4 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => { includeHasUnreadNotes: true, includeSecrets: isSecure })); - - // Update lastUsedAt - User.update({ _id: user._id }, { - $set: { - lastUsedAt: new Date() - } - }); })); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index b1ddd40d13..028d67a018 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -40,6 +40,16 @@ export const meta = { markAsRead: { validator: $.bool.optional, default: true + }, + + includeTypes: { + validator: $.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote', 'receiveFollowRequest'])).optional, + default: [] as string[] + }, + + excludeTypes: { + validator: $.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote', 'receiveFollowRequest'])).optional, + default: [] as string[] } } }; @@ -89,6 +99,16 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { }; } + if (ps.includeTypes.length > 0) { + query.type = { + $in: ps.includeTypes + }; + } else if (ps.excludeTypes.length > 0) { + query.type = { + $nin: ps.excludeTypes + }; + } + const notifications = await Notification .find(query, { limit: ps.limit, diff --git a/src/server/api/endpoints/i/read_all_messaging_messages.ts b/src/server/api/endpoints/i/read_all_messaging_messages.ts new file mode 100644 index 0000000000..a1fe82c4cb --- /dev/null +++ b/src/server/api/endpoints/i/read_all_messaging_messages.ts @@ -0,0 +1,42 @@ +import User from '../../../../models/user'; +import { publishMainStream } from '../../../../stream'; +import Message from '../../../../models/messaging-message'; +import define from '../../define'; + +export const meta = { + desc: { + 'ja-JP': 'トークメッセージをすべて既読にします。', + 'en-US': 'Mark all talk messages as read.' + }, + + requireCredential: true, + + kind: 'account-write', + + params: { + } +}; + +export default define(meta, (ps, user) => new Promise(async (res, rej) => { + // Update documents + await Message.update({ + recipientId: user._id, + isRead: false + }, { + $set: { + isRead: true + } + }, { + multi: true + }); + + User.update({ _id: user._id }, { + $set: { + hasUnreadMessagingMessage: false + } + }); + + publishMainStream(user._id, 'readAllMessagingMessages'); + + res(); +})); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 4952b2f010..ec6aaa04da 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -6,6 +6,9 @@ import acceptAllFollowRequests from '../../../../services/following/requests/acc import { publishToFollowers } from '../../../../services/i/update'; import define from '../../define'; import getDriveFileUrl from '../../../../misc/get-drive-file-url'; +import parse from '../../../../mfm/parse'; +import extractEmojis from '../../../../misc/extract-emojis'; +const langmap = require('langmap'); export const meta = { desc: { @@ -32,6 +35,13 @@ export const meta = { } }, + lang: { + validator: $.str.optional.nullable.or(Object.keys(langmap)), + desc: { + 'ja-JP': '言語' + } + }, + location: { validator: $.str.optional.nullable.pipe(isValidLocation), desc: { @@ -84,6 +94,13 @@ export const meta = { } }, + autoAcceptFollowed: { + validator: $.bool.optional, + desc: { + 'ja-JP': 'フォローしているユーザーからのフォローリクエストを自動承認するか' + } + }, + isBot: { validator: $.bool.optional, desc: { @@ -121,6 +138,7 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => { if (ps.name !== undefined) updates.name = ps.name; if (ps.description !== undefined) updates.description = ps.description; + if (ps.lang !== undefined) updates.lang = ps.lang; 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; @@ -129,6 +147,7 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => { if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot; if (typeof ps.carefulBot == 'boolean') updates.carefulBot = ps.carefulBot; + if (typeof ps.autoAcceptFollowed == 'boolean') updates.autoAcceptFollowed = ps.autoAcceptFollowed; 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; @@ -182,6 +201,24 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => { } } + //#region emojis + if (updates.name != null || updates.description != null) { + let emojis = [] as string[]; + + if (updates.name != null) { + const tokens = parse(updates.name, true); + emojis = emojis.concat(extractEmojis(tokens)); + } + + if (updates.description != null) { + const tokens = parse(updates.description); + emojis = emojis.concat(extractEmojis(tokens)); + } + + updates.emojis = emojis; + } + //#endregion + await User.update(user._id, { $set: updates }); diff --git a/src/server/api/endpoints/i/update_email.ts b/src/server/api/endpoints/i/update_email.ts new file mode 100644 index 0000000000..e08d1fba05 --- /dev/null +++ b/src/server/api/endpoints/i/update_email.ts @@ -0,0 +1,100 @@ +import $ from 'cafy'; +import User, { pack } from '../../../../models/user'; +import { publishMainStream } from '../../../../stream'; +import define from '../../define'; +import * as nodemailer from 'nodemailer'; +import fetchMeta from '../../../../misc/fetch-meta'; +import rndstr from 'rndstr'; +import config from '../../../../config'; +const ms = require('ms'); +import * as bcrypt from 'bcryptjs'; + +export const meta = { + requireCredential: true, + + secure: true, + + limit: { + duration: ms('1hour'), + max: 3 + }, + + params: { + password: { + validator: $.str + }, + + email: { + validator: $.str.optional.nullable + }, + } +}; + +export default define(meta, (ps, user) => new Promise(async (res, rej) => { + // Compare password + const same = await bcrypt.compare(ps.password, user.password); + + if (!same) { + return rej('incorrect password'); + } + + await User.update(user._id, { + $set: { + email: ps.email, + emailVerified: false, + emailVerifyCode: null + } + }); + + // Serialize + const iObj = await pack(user._id, user, { + detail: true, + includeSecrets: true + }); + + // Send response + res(iObj); + + // Publish meUpdated event + publishMainStream(user._id, 'meUpdated', iObj); + + if (ps.email != null) { + const code = rndstr('a-z0-9', 16); + + await User.update(user._id, { + $set: { + emailVerifyCode: code + } + }); + + const meta = await fetchMeta(); + + const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; + + const transporter = nodemailer.createTransport({ + host: meta.smtpHost, + port: meta.smtpPort, + secure: meta.smtpSecure, + ignoreTLS: !enableAuth, + auth: enableAuth ? { + user: meta.smtpUser, + pass: meta.smtpPass + } : undefined + }); + + const link = `${config.url}/verify-email/${code}`; + + transporter.sendMail({ + from: meta.email, + to: ps.email, + subject: meta.name, + text: `To verify email, please click this link: ${link}` + }, (error, info) => { + if (error) { + return console.error(error); + } + + console.log('Message sent: %s', info.messageId); + }); + } +})); diff --git a/src/server/api/endpoints/i/update_widget.ts b/src/server/api/endpoints/i/update_widget.ts index 90fe8fbe23..da96ec6fc1 100644 --- a/src/server/api/endpoints/i/update_widget.ts +++ b/src/server/api/endpoints/i/update_widget.ts @@ -59,11 +59,11 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { //#region Deck if (widget == null && user.clientSettings.deck && user.clientSettings.deck.columns) { const deck = user.clientSettings.deck; - deck.columns.filter((c: any) => c.type == 'widgets').forEach((c: any) => { - c.widgets.forEach((w: any) => { - if (w.id == ps.id) widget = w; - }); - }); + for (const c of deck.columns.filter((c: any) => c.type == 'widgets')) { + for (const w of c.widgets.filter((w: any) => w.id == ps.id)) { + widget = w; + } + } if (widget) { widget.data = ps.data; diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts index c026e5dd91..78abea269a 100644 --- a/src/server/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -1,7 +1,6 @@ import $ from 'cafy'; -import History from '../../../../models/messaging-history'; import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/messaging-message'; +import Message, { pack, IMessagingMessage } from '../../../../models/messaging-message'; import define from '../../define'; export const meta = { @@ -28,19 +27,36 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { deletedAt: { $exists: false } }); - // Get history - const history = await History - .find({ - userId: user._id, - partnerId: { - $nin: mute.map(m => m.muteeId) - } + const history: IMessagingMessage[] = []; + + for (let i = 0; i < ps.limit; i++) { + const found = history.map(m => m.userId.equals(user._id) ? m.recipientId : m.userId); + + const message = await Message.findOne({ + $or: [{ + userId: user._id + }, { + recipientId: user._id + }], + $and: [{ + userId: { $nin: found }, + recipientId: { $nin: found } + }, { + userId: { $nin: mute.map(m => m.muteeId) }, + recipientId: { $nin: mute.map(m => m.muteeId) } + }] }, { - limit: ps.limit, sort: { - updatedAt: -1 + createdAt: -1 } }); - res(await Promise.all(history.map(h => pack(h.messageId, user)))); + if (message) { + history.push(message); + } else { + break; + } + } + + res(await Promise.all(history.map(h => pack(h._id, user)))); })); diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index f8901449fe..3630dc0d54 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -1,7 +1,6 @@ import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; import Message from '../../../../../models/messaging-message'; import { isValidText } from '../../../../../models/messaging-message'; -import History from '../../../../../models/messaging-history'; import User from '../../../../../models/user'; import Mute from '../../../../../models/mute'; import DriveFile from '../../../../../models/drive-file'; @@ -114,6 +113,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { // 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する setTimeout(async () => { const freshMessage = await Message.findOne({ _id: message._id }, { isRead: true }); + if (freshMessage == null) return; // メッセージが削除されている場合もある if (!freshMessage.isRead) { //#region ただしミュートされているなら発行しない const mute = await Mute.find({ @@ -130,30 +130,4 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { pushSw(message.recipientId, 'unreadMessagingMessage', messageObj); } }, 2000); - - // 履歴作成(自分) - History.update({ - userId: user._id, - partnerId: recipient._id - }, { - updatedAt: new Date(), - userId: user._id, - partnerId: recipient._id, - messageId: message._id - }, { - upsert: true - }); - - // 履歴作成(相手) - History.update({ - userId: recipient._id, - partnerId: user._id - }, { - updatedAt: new Date(), - userId: recipient._id, - partnerId: user._id, - messageId: message._id - }, { - upsert: true - }); })); diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts new file mode 100644 index 0000000000..26161896ce --- /dev/null +++ b/src/server/api/endpoints/messaging/messages/delete.ts @@ -0,0 +1,54 @@ +import $ from 'cafy'; +import ID, { transform } from '../../../../../misc/cafy-id'; +import Message from '../../../../../models/messaging-message'; +import define from '../../../define'; +import { publishMessagingStream } from '../../../../../stream'; +const ms = require('ms'); + +export const meta = { + stability: 'stable', + + desc: { + 'ja-JP': '指定したメッセージを削除します。', + 'en-US': 'Delete a message.' + }, + + requireCredential: true, + + kind: 'messaging-write', + + limit: { + duration: ms('1hour'), + max: 300, + minInterval: ms('1sec') + }, + + params: { + messageId: { + validator: $.type(ID), + transform: transform, + desc: { + 'ja-JP': '対象のメッセージのID', + 'en-US': 'Target message ID.' + } + } + } +}; + +export default define(meta, (ps, user) => new Promise(async (res, rej) => { + const message = await Message.findOne({ + _id: ps.messageId, + userId: user._id + }); + + if (message === null) { + return rej('message not found'); + } + + await Message.remove({ _id: message._id }); + + publishMessagingStream(message.userId, message.recipientId, 'deleted', message._id); + publishMessagingStream(message.recipientId, message.userId, 'deleted', message._id); + + res(); +})); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 9846e95959..3b2a49dbb0 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -42,6 +42,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { clientVersion: client.version, name: instance.name, + uri: config.url, description: instance.description, langs: instance.langs, @@ -58,16 +59,19 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { broadcasts: instance.broadcasts || [], disableRegistration: instance.disableRegistration, disableLocalTimeline: instance.disableLocalTimeline, + disableGlobalTimeline: instance.disableGlobalTimeline, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, cacheRemoteFiles: instance.cacheRemoteFiles, enableRecaptcha: instance.enableRecaptcha, recaptchaSiteKey: instance.recaptchaSiteKey, - swPublickey: config.sw ? config.sw.public_key : null, + swPublickey: instance.swPublicKey, + mascotImageUrl: instance.mascotImageUrl, bannerUrl: instance.bannerUrl, + errorImageUrl: instance.errorImageUrl, maxNoteTextLength: instance.maxNoteTextLength, - emojis: emojis, + enableEmail: instance.enableEmail, enableTwitterIntegration: instance.enableTwitterIntegration, enableGithubIntegration: instance.enableGithubIntegration, @@ -78,14 +82,19 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { response.features = { registration: !instance.disableRegistration, localTimeLine: !instance.disableLocalTimeline, + globalTimeLine: !instance.disableGlobalTimeline, elasticsearch: config.elasticsearch ? true : false, recaptcha: instance.enableRecaptcha, objectStorage: config.drive && config.drive.storage === 'minio', twitter: instance.enableTwitterIntegration, github: instance.enableGithubIntegration, discord: instance.enableDiscordIntegration, - serviceWorker: config.sw ? true : false, - userRecommendation: config.user_recommendation ? config.user_recommendation : {} + serviceWorker: instance.enableServiceWorker, + userRecommendation: { + external: instance.enableExternalUserRecommendation, + engine: instance.externalUserRecommendationEngine, + timeout: instance.externalUserRecommendationTimeout + } }; } @@ -99,6 +108,17 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { response.githubClientSecret = instance.githubClientSecret; response.discordClientId = instance.discordClientId; response.discordClientSecret = instance.discordClientSecret; + response.enableExternalUserRecommendation = instance.enableExternalUserRecommendation; + response.externalUserRecommendationEngine = instance.externalUserRecommendationEngine; + response.externalUserRecommendationTimeout = instance.externalUserRecommendationTimeout; + response.summalyProxy = instance.summalyProxy; + response.email = instance.email; + response.smtpSecure = instance.smtpSecure; + response.smtpHost = instance.smtpHost; + response.smtpPort = instance.smtpPort; + response.smtpUser = instance.smtpUser; + response.smtpPass = instance.smtpPass; + response.swPrivateKey = instance.swPrivateKey; } res(response); diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts index 5edc56165e..b489197076 100644 --- a/src/server/api/endpoints/notes.ts +++ b/src/server/api/endpoints/notes.ts @@ -78,6 +78,7 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { _id: -1 }; const query = { + deletedAt: null, visibility: 'public' } as any; if (ps.sinceId) { @@ -105,9 +106,7 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { const withFiles = ps.withFiles != undefined ? ps.withFiles : ps.media; - if (withFiles) { - query.fileIds = withFiles ? { $exists: true, $ne: null } : []; - } + if (withFiles) query.fileIds = { $exists: true, $ne: null }; if (ps.poll != undefined) { query.poll = ps.poll ? { $exists: true, $ne: null } : null; diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 4f8d6a4f4f..ec84d64975 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -42,7 +42,7 @@ export const meta = { }, visibleUserIds: { - validator: $.arr($.type(ID)).optional.unique().min(1), + validator: $.arr($.type(ID)).optional.unique().min(0), transform: transformMany, desc: { 'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー' @@ -82,6 +82,30 @@ export const meta = { } }, + noExtractMentions: { + validator: $.bool.optional, + default: false, + desc: { + 'ja-JP': '本文からメンションを展開しないか否か。' + } + }, + + noExtractHashtags: { + validator: $.bool.optional, + default: false, + desc: { + 'ja-JP': '本文からハッシュタグを展開しないか否か。' + } + }, + + noExtractEmojis: { + validator: $.bool.optional, + default: false, + desc: { + 'ja-JP': '本文からカスタム絵文字を展開しないか否か。' + } + }, + geo: { validator: $.obj({ coordinates: $.arr().length(2) @@ -219,10 +243,15 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => { } // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー - if ((ps.text == null) && files === null && renote === null && ps.poll == null) { + if (!(ps.text || files.length || renote || ps.poll)) { return rej('text, fileIds, renoteId or poll is required'); } + // 後方互換性のため + if (ps.visibility == 'private') { + ps.visibility = 'specified'; + } + // 投稿を作成 create(user, { createdAt: new Date(), @@ -237,6 +266,9 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => { localOnly: ps.localOnly, visibility: ps.visibility, visibleUsers, + apMentions: ps.noExtractMentions ? [] : undefined, + apHashtags: ps.noExtractHashtags ? [] : undefined, + apEmojis: ps.noExtractEmojis ? [] : undefined, geo: ps.geo }) .then(note => pack(note, user)) diff --git a/src/server/api/endpoints/notes/delete.ts b/src/server/api/endpoints/notes/delete.ts index aa11f7bf19..1923aed9ba 100644 --- a/src/server/api/endpoints/notes/delete.ts +++ b/src/server/api/endpoints/notes/delete.ts @@ -3,6 +3,7 @@ import Note from '../../../../models/note'; import deleteNote from '../../../../services/note/delete'; import User from '../../../../models/user'; import define from '../../define'; +const ms = require('ms'); export const meta = { stability: 'stable', @@ -16,6 +17,12 @@ export const meta = { kind: 'note-write', + limit: { + duration: ms('1hour'), + max: 300, + minInterval: ms('1sec') + }, + params: { noteId: { validator: $.type(ID), diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts index b7f765f27d..f0d052ff98 100644 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ b/src/server/api/endpoints/notes/global-timeline.ts @@ -4,6 +4,7 @@ import Mute from '../../../../models/mute'; import { packMany } from '../../../../models/note'; import define from '../../define'; import { countIf } from '../../../../prelude/array'; +import fetchMeta from '../../../../misc/fetch-meta'; export const meta = { desc: { @@ -51,6 +52,13 @@ export const meta = { }; export default define(meta, (ps, user) => new Promise(async (res, rej) => { + const meta = await fetchMeta(); + if (meta.disableGlobalTimeline) { + if (user == null || (!user.isAdmin && !user.isModerator)) { + return rej('global timeline disabled'); + } + } + // Check if only one of sinceId, untilId, sinceDate, untilDate specified if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { return rej('only one of sinceId, untilId, sinceDate, untilDate can be specified'); diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index 4af182cb5c..8318479d13 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -5,6 +5,8 @@ import { getFriends } from '../../common/get-friends'; import { packMany } from '../../../../models/note'; import define from '../../define'; import { countIf } from '../../../../prelude/array'; +import fetchMeta from '../../../../misc/fetch-meta'; +import activeUsersChart from '../../../../chart/active-users'; export const meta = { desc: { @@ -91,6 +93,11 @@ export const meta = { }; export default define(meta, (ps, user) => new Promise(async (res, rej) => { + const meta = await fetchMeta(); + if (meta.disableLocalTimeline && !user.isAdmin && !user.isModerator) { + return rej('local timeline disabled'); + } + // Check if only one of sinceId, untilId, sinceDate, untilDate specified if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { return rej('only one of sinceId, untilId, sinceDate, untilDate can be specified'); @@ -112,12 +119,10 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { _id: -1 }; - const followQuery = followings.map(f => f.stalk ? { - userId: f.id - } : { + const followQuery = followings.map(f => ({ userId: f.id, - // ストーキングしてないならリプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) + /*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) $or: [{ // リプライでない replyId: null @@ -132,20 +137,40 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { }, { // または // 自分(フォロワー)が送信したリプライ userId: user._id - }] - }); + }]*/ + })); + + const visibleQuery = user == null ? [{ + visibility: { $in: ['public', 'home'] } + }] : [{ + visibility: { $in: ['public', 'home', 'followers'] } + }, { + // myself (for specified/private) + userId: user._id + }, { + // to me (for specified) + visibleUserIds: { $in: [ user._id ] } + }]; const query = { $and: [{ deletedAt: null, $or: [{ - // フォローしている人の投稿 - $or: followQuery + $and: [{ + // フォローしている人の投稿 + $or: followQuery + }, { + // visible for me + $or: visibleQuery + }] }, { // public only visibility: 'public', + // リプライでない + //replyId: null, + // local '_user.host': null }], @@ -249,4 +274,6 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { }); res(await packMany(timeline, user)); + + activeUsersChart.update(user); })); diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index 4446f52cdc..d5b622d1f0 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -4,6 +4,8 @@ import Mute from '../../../../models/mute'; import { packMany } from '../../../../models/note'; import define from '../../define'; import { countIf } from '../../../../prelude/array'; +import fetchMeta from '../../../../misc/fetch-meta'; +import activeUsersChart from '../../../../chart/active-users'; export const meta = { desc: { @@ -66,6 +68,13 @@ export const meta = { }; export default define(meta, (ps, user) => new Promise(async (res, rej) => { + const meta = await fetchMeta(); + if (meta.disableLocalTimeline) { + if (user == null || (!user.isAdmin && !user.isModerator)) { + return rej('local timeline disabled'); + } + } + // Check if only one of sinceId, untilId, sinceDate, untilDate specified if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { return rej('only one of sinceId, untilId, sinceDate, untilDate can be specified'); @@ -87,6 +96,9 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { // public only visibility: 'public', + // リプライでない + //replyId: null, + // local '_user.host': null } as any; @@ -153,4 +165,8 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { }); res(await packMany(timeline, user)); + + if (user) { + activeUsersChart.update(user); + } })); diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index 718f5e4403..4c7c397c77 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -4,6 +4,7 @@ import { getFriendIds } from '../../common/get-friends'; import { packMany } from '../../../../models/note'; import define from '../../define'; import read from '../../../../services/note/read'; +import Mute from '../../../../models/mute'; export const meta = { desc: { @@ -56,6 +57,25 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { }] } as any; + // ミュートしているユーザーを取得 + const mutedUserIds = (await Mute.find({ + muterId: user._id + })).map(m => m.muteeId); + + if (mutedUserIds && mutedUserIds.length > 0) { + query.userId = { + $nin: mutedUserIds + }; + + query['_reply.userId'] = { + $nin: mutedUserIds + }; + + query['_renote.userId'] = { + $nin: mutedUserIds + }; + } + const sort = { _id: -1 }; @@ -91,5 +111,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { res(await packMany(mentions, user)); - mentions.forEach(note => read(user._id, note._id)); + for (const note of mentions) { + read(user._id, note._id); + } })); diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts index cfcba788ec..2bc1a4f913 100644 --- a/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/src/server/api/endpoints/notes/polls/recommendation.ts @@ -39,6 +39,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { const notes = await Note .find({ + '_user.host': null, _id: { $nin: nin }, diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts index dc012152c6..f99fb099c7 100644 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -6,6 +6,8 @@ import watch from '../../../../../services/note/watch'; import { publishNoteStream } from '../../../../../stream'; import notify from '../../../../../notify'; import define from '../../../define'; +import createNote from '../../../../../services/note/create'; +import User from '../../../../../models/user'; export const meta = { desc: { @@ -102,16 +104,31 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { } }) .then(watchers => { - watchers.forEach(watcher => { + for (const watcher of watchers) { notify(watcher.userId, user._id, 'poll_vote', { noteId: note._id, choice: ps.choice }); - }); + } }); // この投稿をWatchする if (user.settings.autoWatch !== false) { watch(user._id, note); } + + // リモート投票の場合リプライ送信 + if (note._user.host != null) { + const pollOwner = await User.findOne({ + _id: note.userId + }); + + createNote(user, { + createdAt: new Date(), + text: ps.choice.toString(), + reply: note, + visibility: 'specified', + visibleUsers: [ pollOwner ], + }); + } })); diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts index c9f70d9658..4037e89943 100644 --- a/src/server/api/endpoints/notes/reactions/create.ts +++ b/src/server/api/endpoints/notes/reactions/create.ts @@ -1,8 +1,10 @@ +import * as mongo from 'mongodb'; import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; -import Note from '../../../../../models/note'; -import create from '../../../../../services/note/reaction/create'; +import createReaction from '../../../../../services/note/reaction/create'; import { validateReaction } from '../../../../../models/note-reaction'; import define from '../../../define'; +import { IUser } from '../../../../../models/user'; +import { getValiedNote } from '../../../common/getters'; export const meta = { stability: 'stable', @@ -34,25 +36,12 @@ export const meta = { } }; -export default define(meta, (ps, user) => new Promise(async (res, rej) => { - // Fetch reactee - const note = await Note.findOne({ - _id: ps.noteId - }); - - if (note === null) { - 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) { - rej(e); - } - - res(); +export default define(meta, (ps, user) => new Promise((res, rej) => { + createReactionById(user, ps.noteId, ps.reaction) + .then(r => res(r)).catch(e => rej(e)); })); + +async function createReactionById(user: IUser, noteId: mongo.ObjectID, reaction: string) { + const note = await getValiedNote(noteId); + await createReaction(user, note, reaction); +} diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts index 367538bed5..9ff4edb7f5 100644 --- a/src/server/api/endpoints/notes/reactions/delete.ts +++ b/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,7 +1,10 @@ +import * as mongo from 'mongodb'; import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; -import Reaction from '../../../../../models/note-reaction'; -import Note from '../../../../../models/note'; import define from '../../../define'; +const ms = require('ms'); +import deleteReaction from '../../../../../services/note/reaction/delete'; +import { IUser } from '../../../../../models/user'; +import { getValiedNote } from '../../../common/getters'; export const meta = { desc: { @@ -13,6 +16,12 @@ export const meta = { kind: 'reaction-write', + limit: { + duration: ms('1hour'), + max: 5, + minInterval: ms('3sec') + }, + params: { noteId: { validator: $.type(ID), @@ -25,39 +34,12 @@ export const meta = { } }; -export default define(meta, (ps, user) => new Promise(async (res, rej) => { - // Fetch unreactee - const note = await Note.findOne({ - _id: ps.noteId - }); - - if (note === null) { - return rej('note not found'); - } - - // if already unreacted - const exist = await Reaction.findOne({ - noteId: note._id, - userId: user._id, - deletedAt: { $exists: false } - }); - - if (exist === null) { - return rej('never reacted'); - } - - // Delete reaction - await Reaction.remove({ - _id: exist._id - }); - - res(); - - const dec: any = {}; - dec[`reactionCounts.${exist.reaction}`] = -1; - - // Decrement reactions count - Note.update({ _id: note._id }, { - $inc: dec - }); +export default define(meta, (ps, user) => new Promise((res, rej) => { + deleteReactionById(user, ps.noteId) + .then(r => res(r)).catch(e => rej(e)); })); + +async function deleteReactionById(user: IUser, noteId: mongo.ObjectID) { + const note = await getValiedNote(noteId); + await deleteReaction(user, note); +} diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts index 6046b9b310..8386d60684 100644 --- a/src/server/api/endpoints/notes/replies.ts +++ b/src/server/api/endpoints/notes/replies.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; import Note, { packMany } from '../../../../models/note'; import define from '../../define'; +import Mute from '../../../../models/mute'; export const meta = { desc: { @@ -33,16 +34,25 @@ export const meta = { }; export default define(meta, (ps, user) => new Promise(async (res, rej) => { - // Lookup note - const note = await Note.findOne({ - _id: ps.noteId - }); + // ミュートしているユーザーを取得 + const mutedUserIds = user ? (await Mute.find({ + muterId: user._id + })).map(m => m.muteeId) : null; + + const q = { + replyId: ps.noteId + } as any; - if (note === null) { - return rej('note not found'); + if (mutedUserIds && mutedUserIds.length > 0) { + q['userId'] = { + $nin: mutedUserIds + }; } - const ids = (note._replyIds || []).slice(ps.offset, ps.offset + ps.limit); + const notes = await Note.find(q, { + limit: ps.limit, + skip: ps.offset + }); - res(await packMany(ids, user)); + res(await packMany(notes, user)); })); diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts index fcc33d14f3..db2f716497 100644 --- a/src/server/api/endpoints/notes/search_by_tag.ts +++ b/src/server/api/endpoints/notes/search_by_tag.ts @@ -103,6 +103,18 @@ export const meta = { }; export default define(meta, (ps, me) => new Promise(async (res, rej) => { + const visibleQuery = me == null ? [{ + visibility: { $in: [ 'public', 'home' ] } + }] : [{ + visibility: { $in: [ 'public', 'home' ] } + }, { + // myself (for specified/private) + userId: me._id + }, { + // to me (for specified) + visibleUserIds: { $in: [ me._id ] } + }]; + const q: any = { $and: [ps.tag ? { tagsLower: ps.tag.toLowerCase() @@ -113,7 +125,8 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { })) })) }], - deletedAt: { $exists: false } + deletedAt: { $exists: false }, + $or: visibleQuery }; const push = (x: any) => q.$and.push(x); diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 3c970c03a1..94f4fb72d3 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -5,6 +5,7 @@ import { getFriends } from '../../common/get-friends'; import { packMany } from '../../../../models/note'; import define from '../../define'; import { countIf } from '../../../../prelude/array'; +import activeUsersChart from '../../../../chart/active-users'; export const meta = { desc: { @@ -116,12 +117,10 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { _id: -1 }; - const followQuery = followings.map(f => f.stalk ? { - userId: f.id - } : { + const followQuery = followings.map(f => ({ userId: f.id, - // ストーキングしてないならリプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) + /*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) $or: [{ // リプライでない replyId: null @@ -136,15 +135,32 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { }, { // または // 自分(フォロワー)が送信したリプライ userId: user._id - }] - }); + }]*/ + })); + + const visibleQuery = user == null ? [{ + visibility: { $in: [ 'public', 'home' ] } + }] : [{ + visibility: { $in: [ 'public', 'home' ] } + }, { + // myself (for specified/private) + userId: user._id + }, { + // to me (for specified) + visibleUserIds: { $in: [ user._id ] } + }]; const query = { $and: [{ deletedAt: null, - // フォローしている人の投稿 - $or: followQuery, + $and: [{ + // フォローしている人の投稿 + $or: followQuery + }, { + // visible for me + $or: visibleQuery + }], // mute userId: { @@ -249,4 +265,6 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { // Serialize res(await packMany(timeline, user)); + + activeUsersChart.update(user); })); diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts index 156ffbbc32..861bbd9b29 100644 --- a/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/src/server/api/endpoints/notes/user-list-timeline.ts @@ -4,6 +4,7 @@ import Mute from '../../../../models/mute'; import { packMany } from '../../../../models/note'; import UserList from '../../../../models/user-list'; import define from '../../define'; +import { getFriends } from '../../common/get-friends'; export const meta = { desc: { @@ -101,7 +102,7 @@ export const meta = { }; export default define(meta, (ps, user) => new Promise(async (res, rej) => { - const [list, mutedUserIds] = await Promise.all([ + const [list, followings, mutedUserIds] = await Promise.all([ // リストを取得 // Fetch the list UserList.findOne({ @@ -109,6 +110,10 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { userId: user._id }), + // フォローを取得 + // Fetch following + getFriends(user._id, true, false), + // ミュートしているユーザーを取得 Mute.find({ muterId: user._id @@ -128,7 +133,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { const listQuery = list.userIds.map(u => ({ userId: u, - // リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) + /*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) $or: [{ // リプライでない replyId: null @@ -143,15 +148,33 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { }, { // または // 自分(フォロワー)が送信したリプライ userId: user._id - }] + }]*/ })); + const visibleQuery = [{ + visibility: { $in: ['public', 'home'] } + }, { + // myself (for specified/private) + userId: user._id + }, { + // to me (for specified) + visibleUserIds: { $in: [user._id] } + }, { + visibility: 'followers', + userId: { $in: followings.map(f => f.id) } + }]; + const query = { $and: [{ deletedAt: null, - // リストに入っている人のタイムラインへの投稿 - $or: listQuery, + $and: [{ + // リストに入っている人のタイムラインへの投稿 + $or: listQuery + }, { + // visible for me + $or: visibleQuery + }], // mute userId: { diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts index 26427452fd..095c1b765d 100644 --- a/src/server/api/endpoints/sw/register.ts +++ b/src/server/api/endpoints/sw/register.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import Subscription from '../../../../models/sw-subscription'; -import config from '../../../../config'; import define from '../../define'; +import fetchMeta from '../../../../misc/fetch-meta'; export const meta = { requireCredential: true, @@ -31,10 +31,12 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { deletedAt: { $exists: false } }); + const instance = await fetchMeta(); + if (exist != null) { return res({ state: 'already-subscribed', - key: config.sw.public_key + key: instance.swPublicKey }); } @@ -47,6 +49,6 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { res({ state: 'subscribed', - key: config.sw.public_key + key: instance.swPublicKey }); })); diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts index 203b4a53c8..aef5bd8507 100644 --- a/src/server/api/endpoints/users.ts +++ b/src/server/api/endpoints/users.ts @@ -17,7 +17,23 @@ export const meta = { }, sort: { - validator: $.str.optional.or('+follower|-follower'), + validator: $.str.optional.or([ + '+follower', + '-follower', + '+createdAt', + '-createdAt', + '+updatedAt', + '-updatedAt', + ]), + }, + + origin: { + validator: $.str.optional.or([ + 'combined', + 'local', + 'remote', + ]), + default: 'local' } } }; @@ -33,6 +49,22 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { _sort = { followersCount: 1 }; + } else if (ps.sort == '+createdAt') { + _sort = { + createdAt: -1 + }; + } else if (ps.sort == '+updatedAt') { + _sort = { + updatedAt: -1 + }; + } else if (ps.sort == '-createdAt') { + _sort = { + createdAt: 1 + }; + } else if (ps.sort == '-updatedAt') { + _sort = { + updatedAt: 1 + }; } } else { _sort = { @@ -40,14 +72,17 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { }; } + const q = + ps.origin == 'local' ? { host: null } : + ps.origin == 'remote' ? { host: { $ne: null } } : + {}; + const users = await User - .find({ - host: null - }, { + .find(q, { limit: ps.limit, sort: _sort, skip: ps.offset }); - res(await Promise.all(users.map(user => pack(user, me)))); + res(await Promise.all(users.map(user => pack(user, me, { detail: true })))); })); diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts index cf123b82ba..353ccef1e9 100644 --- a/src/server/api/endpoints/users/get_frequently_replied_users.ts +++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts @@ -2,6 +2,7 @@ import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; import Note from '../../../../models/note'; import User, { pack } from '../../../../models/user'; import define from '../../define'; +import { maximum } from '../../../../prelude/array'; export const meta = { requireCredential: false, @@ -77,20 +78,16 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { const repliedUsers: any = {}; // Extract replies from recent notes - replyTargetNotes.forEach(note => { - const userId = note.userId.toString(); + for (const userId of replyTargetNotes.map(x => x.userId.toString())) { if (repliedUsers[userId]) { repliedUsers[userId]++; } else { repliedUsers[userId] = 1; } - }); + } // Calc peak - let peak = 0; - Object.keys(repliedUsers).forEach(user => { - if (repliedUsers[user] > peak) peak = repliedUsers[user]; - }); + const peak = maximum(Object.values(repliedUsers)); // Sort replies by frequency const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); diff --git a/src/server/api/endpoints/users/lists/pull.ts b/src/server/api/endpoints/users/lists/pull.ts new file mode 100644 index 0000000000..f1b25127b3 --- /dev/null +++ b/src/server/api/endpoints/users/lists/pull.ts @@ -0,0 +1,64 @@ +import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; +import UserList from '../../../../../models/user-list'; +import User, { pack as packUser } from '../../../../../models/user'; +import { publishUserListStream } from '../../../../../stream'; +import define from '../../../define'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーリストから指定したユーザーを削除します。', + 'en-US': 'Remove a user to a user list.' + }, + + requireCredential: true, + + kind: 'account-write', + + params: { + listId: { + validator: $.type(ID), + transform: transform, + }, + + userId: { + validator: $.type(ID), + transform: transform, + desc: { + 'ja-JP': '対象のユーザーのID', + 'en-US': 'Target user ID' + } + }, + } +}; + +export default define(meta, (ps, me) => new Promise(async (res, rej) => { + // Fetch the list + const userList = await UserList.findOne({ + _id: ps.listId, + userId: me._id, + }); + + if (userList == null) { + return rej('list not found'); + } + + // Fetch the user + const user = await User.findOne({ + _id: ps.userId + }); + + if (user == null) { + return rej('user not found'); + } + + // Pull the user + await UserList.update({ _id: userList._id }, { + $pull: { + userIds: user._id + } + }); + + res(); + + publishUserListStream(userList._id, 'userRemoved', await packUser(user)); +})); diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts index e6df1eeece..fdb862de09 100644 --- a/src/server/api/endpoints/users/notes.ts +++ b/src/server/api/endpoints/users/notes.ts @@ -4,6 +4,7 @@ import Note, { packMany } from '../../../../models/note'; import User from '../../../../models/user'; import define from '../../define'; import { countIf } from '../../../../prelude/array'; +import Following from '../../../../models/following'; export const meta = { desc: { @@ -124,6 +125,14 @@ export const meta = { 'ja-JP': '指定された種類のファイルが添付された投稿のみを取得します' } }, + + excludeNsfw: { + validator: $.bool.optional, + default: false, + desc: { + 'ja-JP': 'true にすると、NSFW指定されたファイルを除外します(fileTypeが指定されている場合のみ有効)' + } + }, } }; @@ -152,12 +161,33 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { return rej('user not found'); } + const isFollowing = me == null ? false : ((await Following.findOne({ + followerId: me._id, + followeeId: user._id + })) != null); + //#region Construct query const sort = { } as any; + const visibleQuery = me == null ? [{ + visibility: { $in: ['public', 'home'] } + }] : [{ + visibility: { + $in: isFollowing ? ['public', 'home', 'followers'] : ['public', 'home'] + } + }, { + // myself (for specified/private) + userId: me._id + }, { + // to me (for specified) + visibleUserIds: { $in: [ me._id ] } + }]; + const query = { + $and: [ {} ], deletedAt: null, - userId: user._id + userId: user._id, + $or: visibleQuery } as any; if (ps.sinceId) { @@ -188,6 +218,22 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { query.replyId = null; } + if (ps.includeMyRenotes === false) { + query.$and.push({ + $or: [{ + userId: { $ne: user._id } + }, { + renoteId: null + }, { + text: { $ne: null } + }, { + fileIds: { $ne: [] } + }, { + poll: { $ne: null } + }] + }); + } + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; if (withFiles) { @@ -203,6 +249,12 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { query['_files.contentType'] = { $in: ps.fileType }; + + if (ps.excludeNsfw) { + query['_files.metadata.isSensitive'] = { + $ne: true + }; + } } //#endregion diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index 127029f83c..bacace6a6a 100644 --- a/src/server/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -1,11 +1,13 @@ const ms = require('ms'); import $ from 'cafy'; -import User, { pack } from '../../../../models/user'; +import User, { pack, ILocalUser } from '../../../../models/user'; import { getFriendIds } from '../../common/get-friends'; import Mute from '../../../../models/mute'; -import * as request from 'request'; +import * as request from 'request-promise-native'; import config from '../../../../config'; import define from '../../define'; +import fetchMeta from '../../../../misc/fetch-meta'; +import resolveUser from '../../../../remote/resolve-user'; export const meta = { desc: { @@ -30,13 +32,15 @@ export const meta = { }; export default define(meta, (ps, me) => new Promise(async (res, rej) => { - if (config.user_recommendation && config.user_recommendation.external) { + const instance = await fetchMeta(); + + if (instance.enableExternalUserRecommendation) { const userName = me.username; const hostName = config.hostname; const limit = ps.limit; const offset = ps.offset; - const timeout = config.user_recommendation.timeout; - const engine = config.user_recommendation.engine; + const timeout = instance.externalUserRecommendationTimeout; + const engine = instance.externalUserRecommendationEngine; const url = engine .replace('{{host}}', hostName) .replace('{{user}}', userName) @@ -50,13 +54,10 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { json: true, followRedirect: true, followAllRedirects: true - }, (error: any, response: any, body: any) => { - if (!error && response.statusCode == 200) { - res(body); - } else { - res([]); - } - }); + }) + .then(body => convertUsers(body, me)) + .then(packed => res(packed)) + .catch(e => rej(e)); } else { // ID list of the user itself and other users who the user follows const followingIds = await getFriendIds(me._id); @@ -72,7 +73,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { $nin: followingIds.concat(mutedUserIds) }, isLocked: { $ne: true }, - lastUsedAt: { + updatedAt: { $gte: new Date(Date.now() - ms('7days')) }, host: null @@ -87,3 +88,30 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { res(await Promise.all(users.map(user => pack(user, me, { detail: true })))); } })); + +type IRecommendUser = { + name: string; + username: string; + host: string; + description: string; + avatarUrl: string; +}; + +/** + * Resolve/Pack dummy users + */ +async function convertUsers(src: IRecommendUser[], me: ILocalUser) { + const packed = await Promise.all(src.map(async x => { + const user = await resolveUser(x.username, x.host) + .catch(() => { + console.warn(`Can't resolve ${x.username}@${x.host}`); + return null; + }); + + if (user == null) return x; + + return await pack(user, me, { detail: true }); + })); + + return packed; +} diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts new file mode 100644 index 0000000000..b520b29e23 --- /dev/null +++ b/src/server/api/endpoints/users/report-abuse.ts @@ -0,0 +1,58 @@ +import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; +import define from '../../define'; +import User from '../../../../models/user'; +import AbuseUserReport from '../../../../models/abuse-user-report'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーを迷惑なユーザーであると報告します。' + }, + + requireCredential: true, + + params: { + userId: { + validator: $.type(ID), + transform: transform, + desc: { + 'ja-JP': '対象のユーザーのID', + 'en-US': 'Target user ID' + } + }, + + comment: { + validator: $.str.range(1, 3000), + desc: { + 'ja-JP': '迷惑行為の詳細' + } + }, + } +}; + +export default define(meta, (ps, me) => new Promise(async (res, rej) => { + // Lookup user + const user = await User.findOne({ + _id: ps.userId + }); + + if (user === null) { + return rej('user not found'); + } + + if (user._id.equals(me._id)) { + return rej('cannot report yourself'); + } + + if (user.isAdmin) { + return rej('cannot report admin'); + } + + await AbuseUserReport.insert({ + createdAt: new Date(), + userId: user._id, + reporterId: me._id, + comment: ps.comment + }); + + res(); +})); diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts index edc4d603ca..86b16dcbb1 100644 --- a/src/server/api/endpoints/users/search.ts +++ b/src/server/api/endpoints/users/search.ts @@ -41,11 +41,19 @@ export const meta = { 'ja-JP': 'ローカルユーザーのみ検索対象にするか否か' } }, + + detail: { + validator: $.bool.optional, + default: true, + desc: { + 'ja-JP': '詳細なユーザー情報を含めるか否か' + } + }, }, }; export default define(meta, (ps, me) => new Promise(async (res, rej) => { - const isUsername = validateUsername(ps.query.replace('@', '')); + const isUsername = validateUsername(ps.query.replace('@', ''), !ps.localOnly); let users: IUser[] = []; @@ -70,86 +78,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { users = users.concat(otherUsers); } - - if (users.length < ps.limit) { - const otherUsers = await User - .find({ - _id: { $nin: users.map(u => u._id) }, - host: null, - usernameLower: new RegExp(escapeRegexp(ps.query.replace('@', '').toLowerCase())) - }, { - limit: ps.limit - users.length - }); - - users = users.concat(otherUsers); - } - - if (users.length < ps.limit && !ps.localOnly) { - const otherUsers = await User - .find({ - _id: { $nin: users.map(u => u._id) }, - host: { $ne: null }, - usernameLower: new RegExp(escapeRegexp(ps.query.replace('@', '').toLowerCase())) - }, { - limit: ps.limit - users.length - }); - - users = users.concat(otherUsers); - } - } - - if (users.length < ps.limit) { - const otherUsers = await User - .find({ - _id: { $nin: users.map(u => u._id) }, - host: null, - name: new RegExp('^' + escapeRegexp(ps.query.toLowerCase())) - }, { - limit: ps.limit - users.length - }); - - users = users.concat(otherUsers); - } - - if (users.length < ps.limit && !ps.localOnly) { - const otherUsers = await User - .find({ - _id: { $nin: users.map(u => u._id) }, - host: { $ne: null }, - name: new RegExp('^' + escapeRegexp(ps.query.toLowerCase())) - }, { - limit: ps.limit - users.length - }); - - users = users.concat(otherUsers); - } - - if (users.length < ps.limit) { - const otherUsers = await User - .find({ - _id: { $nin: users.map(u => u._id) }, - host: null, - name: new RegExp(escapeRegexp(ps.query.toLowerCase())) - }, { - limit: ps.limit - users.length - }); - - users = users.concat(otherUsers); - } - - if (users.length < ps.limit && !ps.localOnly) { - const otherUsers = await User - .find({ - _id: { $nin: users.map(u => u._id) }, - host: { $ne: null }, - name: new RegExp(escapeRegexp(ps.query.toLowerCase())) - }, { - limit: ps.limit - users.length - }); - - users = users.concat(otherUsers); } - // Serialize - res(await Promise.all(users.map(user => pack(user, me, { detail: true })))); + res(await Promise.all(users.map(user => pack(user, me, { detail: ps.detail })))); })); diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts index 6e4cf514de..fd26554709 100644 --- a/src/server/api/endpoints/users/show.ts +++ b/src/server/api/endpoints/users/show.ts @@ -80,7 +80,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { })); if (isRemoteUser(user)) { - if (user.updatedAt == null || Date.now() - user.updatedAt.getTime() > 1000 * 60 * 60 * 24) { + if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { resolveRemoteUser(ps.username, ps.host, { }, true); } } |