summaryrefslogtreecommitdiff
path: root/src/server/api/endpoints
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/api/endpoints')
-rw-r--r--src/server/api/endpoints/admin/abuse-user-reports.ts54
-rw-r--r--src/server/api/endpoints/admin/drive/files.ts81
-rw-r--r--src/server/api/endpoints/admin/drive/show-file.ts28
-rw-r--r--src/server/api/endpoints/admin/remove-abuse-user-report.ts32
-rw-r--r--src/server/api/endpoints/admin/reset-password.ts57
-rw-r--r--src/server/api/endpoints/admin/show-user.ts40
-rw-r--r--src/server/api/endpoints/admin/show-users.ts123
-rw-r--r--src/server/api/endpoints/admin/suspend-user.ts4
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts189
-rw-r--r--src/server/api/endpoints/aggregation/hashtags.ts9
-rw-r--r--src/server/api/endpoints/ap/show.ts13
-rw-r--r--src/server/api/endpoints/charts/active-users.ts34
-rw-r--r--src/server/api/endpoints/charts/drive.ts2
-rw-r--r--src/server/api/endpoints/charts/federation.ts2
-rw-r--r--src/server/api/endpoints/charts/hashtag.ts2
-rw-r--r--src/server/api/endpoints/charts/network.ts2
-rw-r--r--src/server/api/endpoints/charts/notes.ts2
-rw-r--r--src/server/api/endpoints/charts/user/drive.ts2
-rw-r--r--src/server/api/endpoints/charts/user/following.ts2
-rw-r--r--src/server/api/endpoints/charts/user/notes.ts2
-rw-r--r--src/server/api/endpoints/charts/user/reactions.ts2
-rw-r--r--src/server/api/endpoints/charts/users.ts2
-rw-r--r--src/server/api/endpoints/drive/files.ts2
-rw-r--r--src/server/api/endpoints/drive/files/check_existence.ts2
-rw-r--r--src/server/api/endpoints/drive/files/create.ts2
-rw-r--r--src/server/api/endpoints/drive/files/delete.ts7
-rw-r--r--src/server/api/endpoints/drive/files/find.ts2
-rw-r--r--src/server/api/endpoints/drive/files/show.ts58
-rw-r--r--src/server/api/endpoints/drive/files/update.ts13
-rw-r--r--src/server/api/endpoints/drive/files/upload_from_url.ts4
-rw-r--r--src/server/api/endpoints/drive/stream.ts2
-rw-r--r--src/server/api/endpoints/following/stalk.ts50
-rw-r--r--src/server/api/endpoints/following/unstalk.ts50
-rw-r--r--src/server/api/endpoints/games/reversi/games/show.ts3
-rw-r--r--src/server/api/endpoints/games/reversi/match.ts2
-rw-r--r--src/server/api/endpoints/hashtags/trend.ts7
-rw-r--r--src/server/api/endpoints/i.ts9
-rw-r--r--src/server/api/endpoints/i/notifications.ts20
-rw-r--r--src/server/api/endpoints/i/read_all_messaging_messages.ts42
-rw-r--r--src/server/api/endpoints/i/update.ts37
-rw-r--r--src/server/api/endpoints/i/update_email.ts100
-rw-r--r--src/server/api/endpoints/i/update_widget.ts10
-rw-r--r--src/server/api/endpoints/messaging/history.ts40
-rw-r--r--src/server/api/endpoints/messaging/messages/create.ts28
-rw-r--r--src/server/api/endpoints/messaging/messages/delete.ts54
-rw-r--r--src/server/api/endpoints/meta.ts28
-rw-r--r--src/server/api/endpoints/notes.ts5
-rw-r--r--src/server/api/endpoints/notes/create.ts36
-rw-r--r--src/server/api/endpoints/notes/delete.ts7
-rw-r--r--src/server/api/endpoints/notes/global-timeline.ts8
-rw-r--r--src/server/api/endpoints/notes/hybrid-timeline.ts43
-rw-r--r--src/server/api/endpoints/notes/local-timeline.ts16
-rw-r--r--src/server/api/endpoints/notes/mentions.ts24
-rw-r--r--src/server/api/endpoints/notes/polls/recommendation.ts1
-rw-r--r--src/server/api/endpoints/notes/polls/vote.ts21
-rw-r--r--src/server/api/endpoints/notes/reactions/create.ts35
-rw-r--r--src/server/api/endpoints/notes/reactions/delete.ts56
-rw-r--r--src/server/api/endpoints/notes/replies.ts26
-rw-r--r--src/server/api/endpoints/notes/search_by_tag.ts15
-rw-r--r--src/server/api/endpoints/notes/timeline.ts34
-rw-r--r--src/server/api/endpoints/notes/user-list-timeline.ts33
-rw-r--r--src/server/api/endpoints/sw/register.ts8
-rw-r--r--src/server/api/endpoints/users.ts45
-rw-r--r--src/server/api/endpoints/users/get_frequently_replied_users.ts11
-rw-r--r--src/server/api/endpoints/users/lists/pull.ts64
-rw-r--r--src/server/api/endpoints/users/notes.ts54
-rw-r--r--src/server/api/endpoints/users/recommendation.ts54
-rw-r--r--src/server/api/endpoints/users/report-abuse.ts58
-rw-r--r--src/server/api/endpoints/users/search.ts91
-rw-r--r--src/server/api/endpoints/users/show.ts2
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);
}
}