summaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/activitypub.ts20
-rw-r--r--src/server/activitypub/outbox.ts34
-rw-r--r--src/server/api/call.ts6
-rw-r--r--src/server/api/endpoints.ts2
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts22
-rw-r--r--src/server/api/endpoints/aggregation/hashtags.ts66
-rw-r--r--src/server/api/endpoints/hashtags/trend.ts11
-rw-r--r--src/server/api/endpoints/i/update.ts4
-rw-r--r--src/server/api/endpoints/messaging/messages/create.ts2
-rw-r--r--src/server/api/endpoints/meta.ts7
-rw-r--r--src/server/api/endpoints/notes.ts114
-rw-r--r--src/server/api/endpoints/notes/create.ts23
-rw-r--r--src/server/api/endpoints/notes/global-timeline.ts82
-rw-r--r--src/server/api/endpoints/notes/hybrid-timeline.ts23
-rw-r--r--src/server/api/endpoints/notes/local-timeline.ts96
-rw-r--r--src/server/api/endpoints/notes/search_by_tag.ts200
-rw-r--r--src/server/api/endpoints/notes/timeline.ts23
-rw-r--r--src/server/api/endpoints/notes/trend.ts2
-rw-r--r--src/server/api/endpoints/notes/user-list-timeline.ts20
-rw-r--r--src/server/api/endpoints/sw/register.ts13
-rw-r--r--src/server/api/endpoints/users/lists/delete.ts43
-rw-r--r--src/server/api/endpoints/users/lists/update.ts56
-rw-r--r--src/server/api/endpoints/users/notes.ts171
-rw-r--r--src/server/api/stream/local-timeline.ts6
-rw-r--r--src/server/api/stream/notes-stats.ts2
-rw-r--r--src/server/api/stream/server-stats.ts2
-rw-r--r--src/server/api/streaming.ts6
-rw-r--r--src/server/web/docs.ts2
-rw-r--r--src/server/web/index.ts2
-rw-r--r--src/server/web/views/note.pug5
-rw-r--r--src/server/web/views/user.pug11
31 files changed, 748 insertions, 328 deletions
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index 1007790ca6..3d346693d8 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -10,7 +10,7 @@ import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
import renderNote from '../remote/activitypub/renderer/note';
import renderKey from '../remote/activitypub/renderer/key';
import renderPerson from '../remote/activitypub/renderer/person';
-import Outbox from './activitypub/outbox';
+import Outbox, { packActivity } from './activitypub/outbox';
import Followers from './activitypub/followers';
import Following from './activitypub/following';
@@ -22,7 +22,7 @@ const router = new Router();
function inbox(ctx: Router.IRouterContext) {
let signature;
- ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature;
+ ctx.req.headers.authorization = `Signature ${ctx.req.headers.signature}`;
try {
signature = httpSignature.parseRequest(ctx.req, { 'headers': [] });
@@ -77,6 +77,22 @@ router.get('/notes/:note', async (ctx, next) => {
setResponseType(ctx);
});
+// note activity
+router.get('/notes/:note/activity', async ctx => {
+ const note = await Note.findOne({
+ _id: new mongo.ObjectID(ctx.params.note),
+ visibility: { $in: ['public', 'home'] }
+ });
+
+ if (note === null) {
+ ctx.status = 404;
+ return;
+ }
+
+ ctx.body = pack(await packActivity(note));
+ setResponseType(ctx);
+});
+
// outbox
router.get('/users/:user/outbox', Outbox);
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index 37df190880..1d062f61a1 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -8,8 +8,11 @@ import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-c
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
import { setResponseType } from '../activitypub';
-import Note from '../../models/note';
+import Note, { INote } from '../../models/note';
import renderNote from '../../remote/activitypub/renderer/note';
+import renderCreate from '../../remote/activitypub/renderer/create';
+import renderAnnounce from '../../remote/activitypub/renderer/announce';
+import { countIf } from '../../prelude/array';
export default async (ctx: Router.IRouterContext) => {
const userId = new mongo.ObjectID(ctx.params.user);
@@ -25,7 +28,7 @@ export default async (ctx: Router.IRouterContext) => {
const page: boolean = ctx.request.query.page === 'true';
// Validate parameters
- if (sinceIdErr || untilIdErr || pageErr || [sinceId, untilId].filter(x => x != null).length > 1) {
+ if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) {
ctx.status = 400;
return;
}
@@ -52,15 +55,7 @@ export default async (ctx: Router.IRouterContext) => {
const query = {
userId: user._id,
- $and: [{
- $or: [ { visibility: 'public' }, { visibility: 'home' } ]
- }, { // exclude renote, but include quote
- $or: [{
- text: { $ne: null }
- }, {
- mediaIds: { $ne: [] }
- }]
- }]
+ visibility: { $in: ['public', 'home'] }
} as any;
if (sinceId) {
@@ -84,10 +79,10 @@ export default async (ctx: Router.IRouterContext) => {
if (sinceId) notes.reverse();
- const renderedNotes = await Promise.all(notes.map(note => renderNote(note, false)));
+ const activities = await Promise.all(notes.map(note => packActivity(note)));
const rendered = renderOrderedCollectionPage(
`${partOf}?page=true${sinceId ? `&since_id=${sinceId}` : ''}${untilId ? `&until_id=${untilId}` : ''}`,
- user.notesCount, renderedNotes, partOf,
+ user.notesCount, activities, partOf,
notes.length > 0 ? `${partOf}?page=true&since_id=${notes[0]._id}` : null,
notes.length > 0 ? `${partOf}?page=true&until_id=${notes[notes.length - 1]._id}` : null
);
@@ -104,3 +99,16 @@ export default async (ctx: Router.IRouterContext) => {
setResponseType(ctx);
}
};
+
+/**
+ * Pack Create<Note> or Announce Activity
+ * @param note Note
+ */
+export async function packActivity(note: INote): Promise<object> {
+ if (note.renoteId && note.text == null && note.poll == null && (note.fileIds == null || note.fileIds.length == 0)) {
+ const renote = await Note.findOne(note.renoteId);
+ return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`, note);
+ }
+
+ return renderCreate(await renderNote(note, false), note);
+}
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
index e9abc11f54..ee79e0a13c 100644
--- a/src/server/api/call.ts
+++ b/src/server/api/call.ts
@@ -25,10 +25,8 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any)
return rej('YOU_ARE_NOT_ADMIN');
}
- if (app && ep.meta.kind) {
- if (!app.permission.some(p => p === ep.meta.kind)) {
- return rej('PERMISSION_DENIED');
- }
+ if (app && ep.meta.kind && !app.permission.some(p => p === ep.meta.kind)) {
+ return rej('PERMISSION_DENIED');
}
if (ep.meta.requireCredential && ep.meta.limit) {
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index d4a44070e6..2b00094269 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -79,7 +79,7 @@ const files = glob.sync('**/*.js', {
});
const endpoints: IEndpoint[] = files.map(f => {
- const ep = require('./endpoints/' + f);
+ const ep = require(`./endpoints/${f}`);
return {
name: f.replace('.js', ''),
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index 2c7929fabe..f903628774 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -11,11 +11,23 @@ export const meta = {
requireAdmin: true,
params: {
+ broadcasts: $.arr($.obj()).optional.nullable.note({
+ desc: {
+ 'ja-JP': 'ブロードキャスト'
+ }
+ }),
+
disableRegistration: $.bool.optional.nullable.note({
desc: {
'ja-JP': '招待制か否か'
}
}),
+
+ hidedTags: $.arr($.str).optional.nullable.note({
+ desc: {
+ 'ja-JP': '統計などで無視するハッシュタグ'
+ }
+ }),
}
};
@@ -25,10 +37,18 @@ export default (params: any) => new Promise(async (res, rej) => {
const set = {} as any;
- if (ps.disableRegistration === true || ps.disableRegistration === false) {
+ if (ps.broadcasts) {
+ set.broadcasts = ps.broadcasts;
+ }
+
+ if (typeof ps.disableRegistration === 'boolean') {
set.disableRegistration = ps.disableRegistration;
}
+ if (Array.isArray(ps.hidedTags)) {
+ set.hidedTags = ps.hidedTags;
+ }
+
await Meta.update({}, {
$set: set
}, { upsert: true });
diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts
new file mode 100644
index 0000000000..c5aacd89cd
--- /dev/null
+++ b/src/server/api/endpoints/aggregation/hashtags.ts
@@ -0,0 +1,66 @@
+import Note from '../../../../models/note';
+import Meta from '../../../../models/meta';
+
+export default () => new Promise(async (res, rej) => {
+ const meta = await Meta.findOne({});
+ const hidedTags = (meta.hidedTags || []).map(t => t.toLowerCase());
+
+ const span = 1000 * 60 * 60 * 24 * 7; // 1週間
+
+ //#region 1. 指定期間の内に投稿されたハッシュタグ(とユーザーのペア)を集計
+ const data = await Note.aggregate([{
+ $match: {
+ createdAt: {
+ $gt: new Date(Date.now() - span)
+ },
+ tagsLower: {
+ $exists: true,
+ $ne: []
+ }
+ }
+ }, {
+ $unwind: '$tagsLower'
+ }, {
+ $group: {
+ _id: { tag: '$tagsLower', userId: '$userId' }
+ }
+ }]) as Array<{
+ _id: {
+ tag: string;
+ userId: any;
+ }
+ }>;
+ //#endregion
+
+ if (data.length == 0) {
+ return res([]);
+ }
+
+ let tags: Array<{
+ name: string;
+ count: number;
+ }> = [];
+
+ // カウント
+ data.map(x => x._id).forEach(x => {
+ // ブラックリストに登録されているタグなら弾く
+ if (hidedTags.includes(x.tag)) return;
+
+ const i = tags.findIndex(tag => tag.name == x.tag);
+ if (i != -1) {
+ tags[i].count++;
+ } else {
+ tags.push({
+ name: x.tag,
+ count: 1
+ });
+ }
+ });
+
+ // タグを人気順に並べ替え
+ tags = tags.sort((a, b) => b.count - a.count);
+
+ tags = tags.slice(0, 30);
+
+ res(tags);
+});
diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts
index 01dfccc71c..bfa475619c 100644
--- a/src/server/api/endpoints/hashtags/trend.ts
+++ b/src/server/api/endpoints/hashtags/trend.ts
@@ -1,4 +1,6 @@
import Note from '../../../../models/note';
+import { erase } from '../../../../prelude/array';
+import Meta from '../../../../models/meta';
/*
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
@@ -16,6 +18,9 @@ const max = 5;
* Get trends of hashtags
*/
export default () => new Promise(async (res, rej) => {
+ const meta = await Meta.findOne({});
+ const hidedTags = (meta.hidedTags || []).map(t => t.toLowerCase());
+
//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計
const data = await Note.aggregate([{
$match: {
@@ -52,6 +57,9 @@ export default () => new Promise(async (res, rej) => {
// カウント
data.map(x => x._id).forEach(x => {
+ // ブラックリストに登録されているタグなら弾く
+ if (hidedTags.includes(x.tag)) return;
+
const i = tags.findIndex(tag => tag.name == x.tag);
if (i != -1) {
tags[i].count++;
@@ -85,8 +93,7 @@ export default () => new Promise(async (res, rej) => {
//#endregion
// タグを人気順に並べ替え
- let hots = (await Promise.all(hotsPromises))
- .filter(x => x != null)
+ let hots = erase(null, await Promise.all(hotsPromises))
.sort((a, b) => b.count - a.count)
.map(tag => tag.name)
.slice(0, max);
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index cdb4eb3f56..585339e249 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -5,6 +5,7 @@ import DriveFile from '../../../../models/drive-file';
import acceptAllFollowRequests from '../../../../services/following/requests/accept-all';
import { IApp } from '../../../../models/app';
import config from '../../../../config';
+import { publishToFollowers } from '../../../../services/i/update';
export const meta = {
desc: {
@@ -144,4 +145,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a
if (user.isLocked && isLocked === false) {
acceptAllFollowRequests(user);
}
+
+ // フォロワーにUpdateを配信
+ publishToFollowers(user._id);
});
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index a6fabcfa45..9a49e09248 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -74,7 +74,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
createdAt: new Date(),
fileId: file ? file._id : undefined,
recipientId: recipient._id,
- text: text ? text : undefined,
+ text: text ? text.trim() : undefined,
userId: user._id,
isRead: false
});
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 2b39f26b8e..4472d8d779 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -4,6 +4,7 @@
import * as os from 'os';
import config from '../../../config';
import Meta from '../../../models/meta';
+import { ILocalUser } from '../../../models/user';
const pkg = require('../../../../package.json');
const client = require('../../../../built/client/meta.json');
@@ -11,7 +12,7 @@ const client = require('../../../../built/client/meta.json');
/**
* Show core info
*/
-export default () => new Promise(async (res, rej) => {
+export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
const meta: any = (await Meta.findOne()) || {};
res({
@@ -33,7 +34,9 @@ export default () => new Promise(async (res, rej) => {
},
broadcasts: meta.broadcasts,
disableRegistration: meta.disableRegistration,
+ driveCapacityPerLocalUserMb: config.localDriveCapacityMb,
recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null,
- swPublickey: config.sw ? config.sw.public_key : null
+ swPublickey: config.sw ? config.sw.public_key : null,
+ hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined
});
});
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index 029bc1a95e..5fa58d19de 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -1,51 +1,65 @@
-/**
- * Module dependencies
- */
import $ from 'cafy'; import ID from '../../../misc/cafy-id';
import Note, { pack } from '../../../models/note';
+import getParams from '../get-params';
-/**
- * Get all notes
- */
-export default (params: any) => new Promise(async (res, rej) => {
- // Get 'local' parameter
- const [local, localErr] = $.bool.optional.get(params.local);
- if (localErr) return rej('invalid local param');
+export const meta = {
+ desc: {
+ 'ja-JP': '投稿を取得します。'
+ },
+
+ params: {
+ local: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ローカルの投稿に限定するか否か'
+ }
+ }),
+
+ reply: $.bool.optional.note({
+ desc: {
+ 'ja-JP': '返信に限定するか否か'
+ }
+ }),
- // Get 'reply' parameter
- const [reply, replyErr] = $.bool.optional.get(params.reply);
- if (replyErr) return rej('invalid reply param');
+ renote: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'Renoteに限定するか否か'
+ }
+ }),
- // Get 'renote' parameter
- const [renote, renoteErr] = $.bool.optional.get(params.renote);
- if (renoteErr) return rej('invalid renote param');
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か'
+ }
+ }),
- // Get 'media' parameter
- const [media, mediaErr] = $.bool.optional.get(params.media);
- if (mediaErr) return rej('invalid media param');
+ media: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ }
+ }),
- // Get 'poll' parameter
- const [poll, pollErr] = $.bool.optional.get(params.poll);
- if (pollErr) return rej('invalid poll param');
+ poll: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'アンケートが添付された投稿に限定するか否か'
+ }
+ }),
- // Get 'bot' parameter
- //const [bot, botErr] = $.bool.optional.get(params.bot);
- //if (botErr) return rej('invalid bot param');
+ limit: $.num.optional.range(1, 100).note({
+ default: 10
+ }),
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
- if (limitErr) return rej('invalid limit param');
+ sinceId: $.type(ID).optional.note({}),
- // Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
- if (sinceIdErr) return rej('invalid sinceId param');
+ untilId: $.type(ID).optional.note({}),
+ }
+};
- // Get 'untilId' parameter
- const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
- if (untilIdErr) return rej('invalid untilId param');
+export default (params: any) => new Promise(async (res, rej) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
// Check if both of sinceId and untilId is specified
- if (sinceId && untilId) {
+ if (ps.sinceId && ps.untilId) {
return rej('cannot set sinceId and untilId');
}
@@ -56,35 +70,37 @@ export default (params: any) => new Promise(async (res, rej) => {
const query = {
visibility: 'public'
} as any;
- if (sinceId) {
+ if (ps.sinceId) {
sort._id = 1;
query._id = {
- $gt: sinceId
+ $gt: ps.sinceId
};
- } else if (untilId) {
+ } else if (ps.untilId) {
query._id = {
- $lt: untilId
+ $lt: ps.untilId
};
}
- if (local) {
+ if (ps.local) {
query['_user.host'] = null;
}
- if (reply != undefined) {
- query.replyId = reply ? { $exists: true, $ne: null } : null;
+ if (ps.reply != undefined) {
+ query.replyId = ps.reply ? { $exists: true, $ne: null } : null;
}
- if (renote != undefined) {
- query.renoteId = renote ? { $exists: true, $ne: null } : null;
+ if (ps.renote != undefined) {
+ query.renoteId = ps.renote ? { $exists: true, $ne: null } : null;
}
- if (media != undefined) {
- query.mediaIds = media ? { $exists: true, $ne: null } : [];
+ const withFiles = ps.withFiles != undefined ? ps.withFiles : ps.media;
+
+ if (withFiles) {
+ query.fileIds = withFiles ? { $exists: true, $ne: null } : [];
}
- if (poll != undefined) {
- query.poll = poll ? { $exists: true, $ne: null } : null;
+ if (ps.poll != undefined) {
+ query.poll = ps.poll ? { $exists: true, $ne: null } : null;
}
// TODO
@@ -95,7 +111,7 @@ export default (params: any) => new Promise(async (res, rej) => {
// Issue query
const notes = await Note
.find(query, {
- limit: limit,
+ limit: ps.limit,
sort: sort
});
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index 04f5f7562e..47b53c943b 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -71,9 +71,15 @@ export const meta = {
ref: 'geo'
}),
+ fileIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({
+ desc: {
+ 'ja-JP': '添付するファイル'
+ }
+ }),
+
mediaIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({
desc: {
- 'ja-JP': '添付するメディア'
+ 'ja-JP': '添付するファイル (このパラメータは廃止予定です。代わりに fileIds を使ってください。)'
}
}),
@@ -124,15 +130,16 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
}
let files: IDriveFile[] = [];
- if (ps.mediaIds !== undefined) {
+ const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
+ if (fileIds != null) {
// Fetch files
// forEach だと途中でエラーなどがあっても return できないので
// 敢えて for を使っています。
- for (const mediaId of ps.mediaIds) {
+ for (const fileId of fileIds) {
// Fetch file
// SELECT _id
const entity = await DriveFile.findOne({
- _id: mediaId,
+ _id: fileId,
'metadata.userId': user._id
});
@@ -155,7 +162,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
if (renote == null) {
return rej('renoteee is not found');
- } else if (renote.renoteId && !renote.text && !renote.mediaIds) {
+ } else if (renote.renoteId && !renote.text && !renote.fileIds) {
return rej('cannot renote to renote');
}
}
@@ -176,7 +183,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
}
// 返信対象が引用でないRenoteだったらエラー
- if (reply.renoteId && !reply.text && !reply.mediaIds) {
+ if (reply.renoteId && !reply.text && !reply.fileIds) {
return rej('cannot reply to renote');
}
}
@@ -191,13 +198,13 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
if ((ps.text === undefined || ps.text === null) && files === null && renote === null && ps.poll === undefined) {
- return rej('text, mediaIds, renoteId or poll is required');
+ return rej('text, fileIds, renoteId or poll is required');
}
// 投稿を作成
const note = await create(user, {
createdAt: new Date(),
- media: files,
+ files: files,
poll: ps.poll,
text: ps.text,
reply,
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 8f7233e308..5d93cd78ec 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -3,40 +3,50 @@ import Note from '../../../../models/note';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+import { countIf } from '../../../../prelude/array';
-/**
- * Get timeline of global
- */
-export default async (params: any, user: ILocalUser) => {
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
- if (limitErr) throw 'invalid limit param';
+export const meta = {
+ desc: {
+ 'ja-JP': 'グローバルタイムラインを取得します。'
+ },
+
+ params: {
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か'
+ }
+ }),
+
+ mediaOnly: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ }
+ }),
+
+ limit: $.num.optional.range(1, 100).note({
+ default: 10
+ }),
+
+ sinceId: $.type(ID).optional.note({}),
- // Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
- if (sinceIdErr) throw 'invalid sinceId param';
+ untilId: $.type(ID).optional.note({}),
- // Get 'untilId' parameter
- const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
- if (untilIdErr) throw 'invalid untilId param';
+ sinceDate: $.num.optional.note({}),
- // Get 'sinceDate' parameter
- const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate);
- if (sinceDateErr) throw 'invalid sinceDate param';
+ untilDate: $.num.optional.note({}),
+ }
+};
- // Get 'untilDate' parameter
- const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate);
- if (untilDateErr) throw 'invalid untilDate param';
+export default async (params: any, user: ILocalUser) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
- if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+ if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
- // Get 'mediaOnly' parameter
- const [mediaOnly, mediaOnlyErr] = $.bool.optional.get(params.mediaOnly);
- if (mediaOnlyErr) throw 'invalid mediaOnly param';
-
// ミュートしているユーザーを取得
const mutedUserIds = user ? (await Mute.find({
muterId: user._id
@@ -68,27 +78,29 @@ export default async (params: any, user: ILocalUser) => {
};
}
- if (mediaOnly) {
- query.mediaIds = { $exists: true, $ne: [] };
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+
+ if (withFiles) {
+ query.fileIds = { $exists: true, $ne: [] };
}
- if (sinceId) {
+ if (ps.sinceId) {
sort._id = 1;
query._id = {
- $gt: sinceId
+ $gt: ps.sinceId
};
- } else if (untilId) {
+ } else if (ps.untilId) {
query._id = {
- $lt: untilId
+ $lt: ps.untilId
};
- } else if (sinceDate) {
+ } else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
- $gt: new Date(sinceDate)
+ $gt: new Date(ps.sinceDate)
};
- } else if (untilDate) {
+ } else if (ps.untilDate) {
query.createdAt = {
- $lt: new Date(untilDate)
+ $lt: new Date(ps.untilDate)
};
}
//#endregion
@@ -96,7 +108,7 @@ export default async (params: any, user: ILocalUser) => {
// Issue query
const timeline = await Note
.find(query, {
- limit: limit,
+ limit: ps.limit,
sort: sort
});
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts
index 2dbb1190c1..0eb7b61830 100644
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -5,10 +5,9 @@ import { getFriends } from '../../common/get-friends';
import { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params';
+import { countIf } from '../../../../prelude/array';
export const meta = {
- name: 'notes/hybrid-timeline',
-
desc: {
'ja-JP': 'ハイブリッドタイムラインを取得します。'
},
@@ -66,9 +65,15 @@ export const meta = {
}
}),
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
+ }
+ }),
+
mediaOnly: $.bool.optional.note({
desc: {
- 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します'
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
}),
}
@@ -82,7 +87,7 @@ export default async (params: any, user: ILocalUser) => {
if (psErr) throw psErr;
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
- if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) {
+ if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
@@ -164,7 +169,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -180,7 +185,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -196,16 +201,16 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
});
}
- if (ps.mediaOnly) {
+ if (ps.withFiles || ps.mediaOnly) {
query.$and.push({
- mediaIds: { $exists: true, $ne: [] }
+ fileIds: { $exists: true, $ne: [] }
});
}
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
index bbcc6303ca..ff10e6fbaa 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -3,40 +3,56 @@ import Note from '../../../../models/note';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+import { countIf } from '../../../../prelude/array';
-/**
- * Get timeline of local
- */
-export default async (params: any, user: ILocalUser) => {
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
- if (limitErr) throw 'invalid limit param';
+export const meta = {
+ desc: {
+ 'ja-JP': 'ローカルタイムラインを取得します。'
+ },
+
+ params: {
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か'
+ }
+ }),
+
+ mediaOnly: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ }
+ }),
+
+ fileType: $.arr($.str).optional.note({
+ desc: {
+ 'ja-JP': '指定された種類のファイルが添付された投稿のみを取得します'
+ }
+ }),
- // Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
- if (sinceIdErr) throw 'invalid sinceId param';
+ limit: $.num.optional.range(1, 100).note({
+ default: 10
+ }),
- // Get 'untilId' parameter
- const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
- if (untilIdErr) throw 'invalid untilId param';
+ sinceId: $.type(ID).optional.note({}),
- // Get 'sinceDate' parameter
- const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate);
- if (sinceDateErr) throw 'invalid sinceDate param';
+ untilId: $.type(ID).optional.note({}),
- // Get 'untilDate' parameter
- const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate);
- if (untilDateErr) throw 'invalid untilDate param';
+ sinceDate: $.num.optional.note({}),
+
+ untilDate: $.num.optional.note({}),
+ }
+};
+
+export default async (params: any, user: ILocalUser) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
- if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+ if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
- // Get 'mediaOnly' parameter
- const [mediaOnly, mediaOnlyErr] = $.bool.optional.get(params.mediaOnly);
- if (mediaOnlyErr) throw 'invalid mediaOnly param';
-
// ミュートしているユーザーを取得
const mutedUserIds = user ? (await Mute.find({
muterId: user._id
@@ -69,27 +85,37 @@ export default async (params: any, user: ILocalUser) => {
};
}
- if (mediaOnly) {
- query.mediaIds = { $exists: true, $ne: [] };
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+
+ if (withFiles) {
+ query.fileIds = { $exists: true, $ne: [] };
+ }
+
+ if (ps.fileType) {
+ query.fileIds = { $exists: true, $ne: [] };
+
+ query['_files.contentType'] = {
+ $in: ps.fileType
+ };
}
- if (sinceId) {
+ if (ps.sinceId) {
sort._id = 1;
query._id = {
- $gt: sinceId
+ $gt: ps.sinceId
};
- } else if (untilId) {
+ } else if (ps.untilId) {
query._id = {
- $lt: untilId
+ $lt: ps.untilId
};
- } else if (sinceDate) {
+ } else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
- $gt: new Date(sinceDate)
+ $gt: new Date(ps.sinceDate)
};
- } else if (untilDate) {
+ } else if (ps.untilDate) {
query.createdAt = {
- $lt: new Date(untilDate)
+ $lt: new Date(ps.untilDate)
};
}
//#endregion
@@ -97,7 +123,7 @@ export default async (params: any, user: ILocalUser) => {
// Issue query
const timeline = await Note
.find(query, {
- limit: limit,
+ limit: ps.limit,
sort: sort
});
diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts
index e092275fe8..77082c2600 100644
--- a/src/server/api/endpoints/notes/search_by_tag.ts
+++ b/src/server/api/endpoints/notes/search_by_tag.ts
@@ -4,119 +4,153 @@ import User, { ILocalUser } from '../../../../models/user';
import Mute from '../../../../models/mute';
import { getFriendIds } from '../../common/get-friends';
import { pack } from '../../../../models/note';
+import getParams from '../../get-params';
+import { erase } from '../../../../prelude/array';
-/**
- * Search notes by tag
- */
-export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
- // Get 'tag' parameter
- const [tag, tagError] = $.str.get(params.tag);
- if (tagError) return rej('invalid tag param');
+export const meta = {
+ desc: {
+ 'ja-JP': '指定されたタグが付けられた投稿を取得します。'
+ },
+
+ params: {
+ tag: $.str.note({
+ desc: {
+ 'ja-JP': 'タグ'
+ }
+ }),
+
+ includeUserIds: $.arr($.type(ID)).optional.note({
+ default: []
+ }),
+
+ excludeUserIds: $.arr($.type(ID)).optional.note({
+ default: []
+ }),
+
+ includeUserUsernames: $.arr($.str).optional.note({
+ default: []
+ }),
+
+ excludeUserUsernames: $.arr($.str).optional.note({
+ default: []
+ }),
+
+ following: $.bool.optional.nullable.note({
+ default: null
+ }),
+
+ mute: $.str.optional.note({
+ default: 'mute_all'
+ }),
- // Get 'includeUserIds' parameter
- const [includeUserIds = [], includeUserIdsErr] = $.arr($.type(ID)).optional.get(params.includeUserIds);
- if (includeUserIdsErr) return rej('invalid includeUserIds param');
+ reply: $.bool.optional.nullable.note({
+ default: null,
- // Get 'excludeUserIds' parameter
- const [excludeUserIds = [], excludeUserIdsErr] = $.arr($.type(ID)).optional.get(params.excludeUserIds);
- if (excludeUserIdsErr) return rej('invalid excludeUserIds param');
+ desc: {
+ 'ja-JP': '返信に限定するか否か'
+ }
+ }),
- // Get 'includeUserUsernames' parameter
- const [includeUserUsernames = [], includeUserUsernamesErr] = $.arr($.str).optional.get(params.includeUserUsernames);
- if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param');
+ renote: $.bool.optional.nullable.note({
+ default: null,
- // Get 'excludeUserUsernames' parameter
- const [excludeUserUsernames = [], excludeUserUsernamesErr] = $.arr($.str).optional.get(params.excludeUserUsernames);
- if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param');
+ desc: {
+ 'ja-JP': 'Renoteに限定するか否か'
+ }
+ }),
- // Get 'following' parameter
- const [following = null, followingErr] = $.bool.optional.nullable.get(params.following);
- if (followingErr) return rej('invalid following param');
+ withFiles: $.bool.optional.nullable.note({
+ default: null,
- // Get 'mute' parameter
- const [mute = 'mute_all', muteErr] = $.str.optional.get(params.mute);
- if (muteErr) return rej('invalid mute param');
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か'
+ }
+ }),
- // Get 'reply' parameter
- const [reply = null, replyErr] = $.bool.optional.nullable.get(params.reply);
- if (replyErr) return rej('invalid reply param');
+ media: $.bool.optional.nullable.note({
+ default: null,
- // Get 'renote' parameter
- const [renote = null, renoteErr] = $.bool.optional.nullable.get(params.renote);
- if (renoteErr) return rej('invalid renote param');
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ }
+ }),
- // Get 'media' parameter
- const [media = null, mediaErr] = $.bool.optional.nullable.get(params.media);
- if (mediaErr) return rej('invalid media param');
+ poll: $.bool.optional.nullable.note({
+ default: null,
- // Get 'poll' parameter
- const [poll = null, pollErr] = $.bool.optional.nullable.get(params.poll);
- if (pollErr) return rej('invalid poll param');
+ desc: {
+ 'ja-JP': 'アンケートが添付された投稿に限定するか否か'
+ }
+ }),
- // Get 'sinceDate' parameter
- const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate);
- if (sinceDateErr) throw 'invalid sinceDate param';
+ sinceDate: $.num.optional.note({
+ }),
- // Get 'untilDate' parameter
- const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate);
- if (untilDateErr) throw 'invalid untilDate param';
+ untilDate: $.num.optional.note({
+ }),
- // Get 'offset' parameter
- const [offset = 0, offsetErr] = $.num.optional.min(0).get(params.offset);
- if (offsetErr) return rej('invalid offset param');
+ offset: $.num.optional.min(0).note({
+ default: 0
+ }),
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 30).get(params.limit);
- if (limitErr) return rej('invalid limit param');
+ limit: $.num.optional.range(1, 30).note({
+ default: 10
+ }),
+ }
+};
+
+export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
- if (includeUserUsernames != null) {
- const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
+ if (ps.includeUserUsernames != null) {
+ const ids = erase(null, await Promise.all(ps.includeUserUsernames.map(async (username) => {
const _user = await User.findOne({
usernameLower: username.toLowerCase()
});
return _user ? _user._id : null;
- }))).filter(id => id != null);
+ })));
- ids.forEach(id => includeUserIds.push(id));
+ ids.forEach(id => ps.includeUserIds.push(id));
}
- if (excludeUserUsernames != null) {
- const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
+ if (ps.excludeUserUsernames != null) {
+ const ids = erase(null, await Promise.all(ps.excludeUserUsernames.map(async (username) => {
const _user = await User.findOne({
usernameLower: username.toLowerCase()
});
return _user ? _user._id : null;
- }))).filter(id => id != null);
+ })));
- ids.forEach(id => excludeUserIds.push(id));
+ ids.forEach(id => ps.excludeUserIds.push(id));
}
let q: any = {
$and: [{
- tagsLower: tag.toLowerCase()
+ tagsLower: ps.tag.toLowerCase()
}]
};
const push = (x: any) => q.$and.push(x);
- if (includeUserIds && includeUserIds.length != 0) {
+ if (ps.includeUserIds && ps.includeUserIds.length != 0) {
push({
userId: {
- $in: includeUserIds
+ $in: ps.includeUserIds
}
});
- } else if (excludeUserIds && excludeUserIds.length != 0) {
+ } else if (ps.excludeUserIds && ps.excludeUserIds.length != 0) {
push({
userId: {
- $nin: excludeUserIds
+ $nin: ps.excludeUserIds
}
});
}
- if (following != null && me != null) {
+ if (ps.following != null && me != null) {
const ids = await getFriendIds(me._id, false);
push({
- userId: following ? {
+ userId: ps.following ? {
$in: ids
} : {
$nin: ids.concat(me._id)
@@ -131,7 +165,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
});
const mutedUserIds = mutes.map(m => m.muteeId);
- switch (mute) {
+ switch (ps.mute) {
case 'mute_all':
push({
userId: {
@@ -202,8 +236,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
}
- if (reply != null) {
- if (reply) {
+ if (ps.reply != null) {
+ if (ps.reply) {
push({
replyId: {
$exists: true,
@@ -223,8 +257,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
}
- if (renote != null) {
- if (renote) {
+ if (ps.renote != null) {
+ if (ps.renote) {
push({
renoteId: {
$exists: true,
@@ -244,10 +278,12 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
}
- if (media != null) {
- if (media) {
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.media;
+
+ if (withFiles != null) {
+ if (withFiles) {
push({
- mediaIds: {
+ fileIds: {
$exists: true,
$ne: null
}
@@ -255,18 +291,18 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
} else {
push({
$or: [{
- mediaIds: {
+ fileIds: {
$exists: false
}
}, {
- mediaIds: null
+ fileIds: null
}]
});
}
}
- if (poll != null) {
- if (poll) {
+ if (ps.poll != null) {
+ if (ps.poll) {
push({
poll: {
$exists: true,
@@ -286,18 +322,18 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
}
- if (sinceDate) {
+ if (ps.sinceDate) {
push({
createdAt: {
- $gt: new Date(sinceDate)
+ $gt: new Date(ps.sinceDate)
}
});
}
- if (untilDate) {
+ if (ps.untilDate) {
push({
createdAt: {
- $lt: new Date(untilDate)
+ $lt: new Date(ps.untilDate)
}
});
}
@@ -312,8 +348,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
sort: {
_id: -1
},
- limit: limit,
- skip: offset
+ limit: ps.limit,
+ skip: ps.offset
});
// Serialize
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index 099bf2010b..5f3844987c 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 { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params';
+import { countIf } from '../../../../prelude/array';
export const meta = {
desc: {
@@ -67,9 +68,15 @@ export const meta = {
}
}),
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
+ }
+ }),
+
mediaOnly: $.bool.optional.note({
desc: {
- 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します'
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
}),
}
@@ -80,7 +87,7 @@ export default async (params: any, user: ILocalUser) => {
if (psErr) throw psErr;
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
- if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) {
+ if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
@@ -154,7 +161,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -170,7 +177,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -186,16 +193,18 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
});
}
- if (ps.mediaOnly) {
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+
+ if (withFiles) {
query.$and.push({
- mediaIds: { $exists: true, $ne: [] }
+ fileIds: { $exists: true, $ne: [] }
});
}
diff --git a/src/server/api/endpoints/notes/trend.ts b/src/server/api/endpoints/notes/trend.ts
index 7a0a098f28..9f55ed3243 100644
--- a/src/server/api/endpoints/notes/trend.ts
+++ b/src/server/api/endpoints/notes/trend.ts
@@ -52,7 +52,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
}
if (media != undefined) {
- query.mediaIds = media ? { $exists: true, $ne: null } : null;
+ query.fileIds = media ? { $exists: true, $ne: null } : null;
}
if (poll != undefined) {
diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts
index a7b43014ed..61192d7d3e 100644
--- a/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -73,9 +73,15 @@ export const meta = {
}
}),
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
+ }
+ }),
+
mediaOnly: $.bool.optional.note({
desc: {
- 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します'
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
}),
}
@@ -160,7 +166,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -176,7 +182,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -192,16 +198,18 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
});
}
- if (ps.mediaOnly) {
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+
+ if (withFiles) {
query.$and.push({
- mediaIds: { $exists: true, $ne: [] }
+ fileIds: { $exists: true, $ne: [] }
});
}
diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts
index 3414600048..503fc94654 100644
--- a/src/server/api/endpoints/sw/register.ts
+++ b/src/server/api/endpoints/sw/register.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
import Subscription from '../../../../models/sw-subscription';
import { ILocalUser } from '../../../../models/user';
+import config from '../../../../config';
export const meta = {
requireCredential: true
@@ -31,8 +32,11 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
deletedAt: { $exists: false }
});
- if (exist !== null) {
- return res();
+ if (exist != null) {
+ return res({
+ state: 'already-subscribed',
+ key: config.sw.public_key
+ });
}
await Subscription.insert({
@@ -42,5 +46,8 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
publickey: publickey
});
- res();
+ res({
+ state: 'subscribed',
+ key: config.sw.public_key
+ });
});
diff --git a/src/server/api/endpoints/users/lists/delete.ts b/src/server/api/endpoints/users/lists/delete.ts
new file mode 100644
index 0000000000..906534922e
--- /dev/null
+++ b/src/server/api/endpoints/users/lists/delete.ts
@@ -0,0 +1,43 @@
+import $ from 'cafy';
+import ID from '../../../../../misc/cafy-id';
+import UserList, { deleteUserList } from '../../../../../models/user-list';
+import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
+
+export const meta = {
+ desc: {
+ 'ja-JP': '指定したユーザーリストを削除します。',
+ 'en-US': 'Delete a user list'
+ },
+
+ requireCredential: true,
+
+ kind: 'account-write',
+
+ params: {
+ listId: $.type(ID).note({
+ desc: {
+ 'ja-JP': '対象となるユーザーリストのID',
+ 'en-US': 'ID of target user list'
+ }
+ })
+ }
+};
+
+export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) return rej(psErr);
+
+ const userList = await UserList.findOne({
+ _id: ps.listId,
+ userId: user._id
+ });
+
+ if (userList == null) {
+ return rej('list not found');
+ }
+
+ deleteUserList(userList);
+
+ res();
+});
diff --git a/src/server/api/endpoints/users/lists/update.ts b/src/server/api/endpoints/users/lists/update.ts
new file mode 100644
index 0000000000..e6577eca4f
--- /dev/null
+++ b/src/server/api/endpoints/users/lists/update.ts
@@ -0,0 +1,56 @@
+import $ from 'cafy';
+import ID from '../../../../../misc/cafy-id';
+import UserList, { pack } from '../../../../../models/user-list';
+import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
+
+export const meta = {
+ desc: {
+ 'ja-JP': '指定したユーザーリストを更新します。',
+ 'en-US': 'Update a user list'
+ },
+
+ requireCredential: true,
+
+ kind: 'account-write',
+
+ params: {
+ listId: $.type(ID).note({
+ desc: {
+ 'ja-JP': '対象となるユーザーリストのID',
+ 'en-US': 'ID of target user list'
+ }
+ }),
+ title: $.str.range(1, 100).note({
+ desc: {
+ 'ja-JP': 'このユーザーリストの名前',
+ 'en-US': 'name of this user list'
+ }
+ })
+ }
+};
+
+export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
+
+ // Fetch the list
+ const userList = await UserList.findOne({
+ _id: ps.listId,
+ userId: user._id
+ });
+
+ if (userList == null) {
+ return rej('list not found');
+ }
+
+ // update
+ await UserList.update({ _id: userList._id }, {
+ $set: {
+ title: ps.title
+ }
+ });
+
+ // Response
+ res(await pack(userList._id));
+});
diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
index ff7855bde0..1ab7786a18 100644
--- a/src/server/api/endpoints/users/notes.ts
+++ b/src/server/api/endpoints/users/notes.ts
@@ -2,63 +2,122 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import getHostLower from '../../common/get-host-lower';
import Note, { pack } from '../../../../models/note';
import User, { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+import { countIf } from '../../../../prelude/array';
-/**
- * Get notes of a user
- */
-export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
- // Get 'userId' parameter
- const [userId, userIdErr] = $.type(ID).optional.get(params.userId);
- if (userIdErr) return rej('invalid userId param');
+export const meta = {
+ desc: {
+ 'ja-JP': '指定したユーザーのタイムラインを取得します。'
+ },
- // Get 'username' parameter
- const [username, usernameErr] = $.str.optional.get(params.username);
- if (usernameErr) return rej('invalid username param');
+ params: {
+ userId: $.type(ID).optional.note({
+ desc: {
+ 'ja-JP': 'ユーザーID'
+ }
+ }),
- if (userId === undefined && username === undefined) {
- return rej('userId or username is required');
- }
+ username: $.str.optional.note({
+ desc: {
+ 'ja-JP': 'ユーザー名'
+ }
+ }),
+
+ host: $.str.optional.note({
+ }),
- // Get 'host' parameter
- const [host, hostErr] = $.str.optional.get(params.host);
- if (hostErr) return rej('invalid host param');
+ includeReplies: $.bool.optional.note({
+ default: true,
- // Get 'includeReplies' parameter
- const [includeReplies = true, includeRepliesErr] = $.bool.optional.get(params.includeReplies);
- if (includeRepliesErr) return rej('invalid includeReplies param');
+ desc: {
+ 'ja-JP': 'リプライを含めるか否か'
+ }
+ }),
- // Get 'withMedia' parameter
- const [withMedia = false, withMediaErr] = $.bool.optional.get(params.withMedia);
- if (withMediaErr) return rej('invalid withMedia param');
+ limit: $.num.optional.range(1, 100).note({
+ default: 10,
+ desc: {
+ 'ja-JP': '最大数'
+ }
+ }),
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
- if (limitErr) return rej('invalid limit param');
+ sinceId: $.type(ID).optional.note({
+ desc: {
+ 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
+ }
+ }),
- // Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
- if (sinceIdErr) return rej('invalid sinceId param');
+ untilId: $.type(ID).optional.note({
+ desc: {
+ 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+ }
+ }),
- // Get 'untilId' parameter
- const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
- if (untilIdErr) return rej('invalid untilId param');
+ sinceDate: $.num.optional.note({
+ desc: {
+ 'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
+ }
+ }),
- // Get 'sinceDate' parameter
- const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate);
- if (sinceDateErr) throw 'invalid sinceDate param';
+ untilDate: $.num.optional.note({
+ desc: {
+ 'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
+ }
+ }),
- // Get 'untilDate' parameter
- const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate);
- if (untilDateErr) throw 'invalid untilDate param';
+ includeMyRenotes: $.bool.optional.note({
+ default: true,
+ desc: {
+ 'ja-JP': '自分の行ったRenoteを含めるかどうか'
+ }
+ }),
+
+ includeRenotedMyNotes: $.bool.optional.note({
+ default: true,
+ desc: {
+ 'ja-JP': 'Renoteされた自分の投稿を含めるかどうか'
+ }
+ }),
+
+ includeLocalRenotes: $.bool.optional.note({
+ default: true,
+ desc: {
+ 'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか'
+ }
+ }),
+
+ withFiles: $.bool.optional.note({
+ default: false,
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
+ }
+ }),
+
+ mediaOnly: $.bool.optional.note({
+ default: false,
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ }
+ }),
+ }
+};
+
+export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
+
+ if (ps.userId === undefined && ps.username === undefined) {
+ return rej('userId or username is required');
+ }
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
- if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+ if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
- const q = userId !== undefined
- ? { _id: userId }
- : { usernameLower: username.toLowerCase(), host: getHostLower(host) } ;
+ const q = ps.userId !== undefined
+ ? { _id: ps.userId }
+ : { usernameLower: ps.username.toLowerCase(), host: getHostLower(ps.host) } ;
// Lookup user
const user = await User.findOne(q, {
@@ -80,32 +139,34 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
userId: user._id
} as any;
- if (sinceId) {
+ if (ps.sinceId) {
sort._id = 1;
query._id = {
- $gt: sinceId
+ $gt: ps.sinceId
};
- } else if (untilId) {
+ } else if (ps.untilId) {
query._id = {
- $lt: untilId
+ $lt: ps.untilId
};
- } else if (sinceDate) {
+ } else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
- $gt: new Date(sinceDate)
+ $gt: new Date(ps.sinceDate)
};
- } else if (untilDate) {
+ } else if (ps.untilDate) {
query.createdAt = {
- $lt: new Date(untilDate)
+ $lt: new Date(ps.untilDate)
};
}
- if (!includeReplies) {
+ if (!ps.includeReplies) {
query.replyId = null;
}
- if (withMedia) {
- query.mediaIds = {
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+
+ if (withFiles) {
+ query.fileIds = {
$exists: true,
$ne: []
};
@@ -115,12 +176,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
// Issue query
const notes = await Note
.find(query, {
- limit: limit,
+ limit: ps.limit,
sort: sort
});
// Serialize
- res(await Promise.all(notes.map(async (note) =>
- await pack(note, me)
- )));
+ res(await Promise.all(notes.map(note => pack(note, me))));
});
diff --git a/src/server/api/stream/local-timeline.ts b/src/server/api/stream/local-timeline.ts
index 82060a7aaa..25e0e00c9f 100644
--- a/src/server/api/stream/local-timeline.ts
+++ b/src/server/api/stream/local-timeline.ts
@@ -9,10 +9,10 @@ export default async function(
request: websocket.request,
connection: websocket.connection,
subscriber: Xev,
- user: IUser
+ user?: IUser
) {
- const mute = await Mute.find({ muterId: user._id });
- const mutedUserIds = mute.map(m => m.muteeId.toString());
+ const mute = user ? await Mute.find({ muterId: user._id }) : null;
+ const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
// Subscribe stream
subscriber.on('local-timeline', async note => {
diff --git a/src/server/api/stream/notes-stats.ts b/src/server/api/stream/notes-stats.ts
index ab00620018..ba99403226 100644
--- a/src/server/api/stream/notes-stats.ts
+++ b/src/server/api/stream/notes-stats.ts
@@ -16,7 +16,7 @@ export default function(request: websocket.request, connection: websocket.connec
switch (msg.type) {
case 'requestLog':
- ev.once('notesStatsLog:' + msg.id, statsLog => {
+ ev.once(`notesStatsLog:${msg.id}`, statsLog => {
connection.send(JSON.stringify({
type: 'statsLog',
body: statsLog
diff --git a/src/server/api/stream/server-stats.ts b/src/server/api/stream/server-stats.ts
index f6c1f14ebe..d4fbeefa04 100644
--- a/src/server/api/stream/server-stats.ts
+++ b/src/server/api/stream/server-stats.ts
@@ -16,7 +16,7 @@ export default function(request: websocket.request, connection: websocket.connec
switch (msg.type) {
case 'requestLog':
- ev.once('serverStatsLog:' + msg.id, statsLog => {
+ ev.once(`serverStatsLog:${msg.id}`, statsLog => {
connection.send(JSON.stringify({
type: 'statsLog',
body: statsLog
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index c8b2d4e0b9..e6094a40b2 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -52,6 +52,11 @@ module.exports = (server: http.Server) => {
return;
}
+ if (request.resourceURL.pathname === '/local-timeline') {
+ localTimelineStream(request, connection, ev, user);
+ return;
+ }
+
if (user == null) {
connection.send('authentication-failed');
connection.close();
@@ -60,7 +65,6 @@ module.exports = (server: http.Server) => {
const channel: any =
request.resourceURL.pathname === '/' ? homeStream :
- request.resourceURL.pathname === '/local-timeline' ? localTimelineStream :
request.resourceURL.pathname === '/hybrid-timeline' ? hybridTimelineStream :
request.resourceURL.pathname === '/global-timeline' ? globalTimelineStream :
request.resourceURL.pathname === '/user-list' ? userListStream :
diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts
index 81e5ace3e8..14ccbdd04f 100644
--- a/src/server/web/docs.ts
+++ b/src/server/web/docs.ts
@@ -196,7 +196,7 @@ router.get('/*/api/entities/*', async ctx => {
const lang = ctx.params[0];
const entity = ctx.params[1];
- const x = yaml.safeLoad(fs.readFileSync(path.resolve(__dirname + '/../../../src/docs/api/entities/' + entity + '.yaml'), 'utf-8')) as any;
+ const x = yaml.safeLoad(fs.readFileSync(path.resolve(`${__dirname}/../../../src/docs/api/entities/${entity}.yaml`), 'utf-8')) as any;
await ctx.render('../../../../src/docs/api/entities/view', Object.assign(await genVars(lang), {
id: `api/entities/${entity}`,
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 452e36fe95..e7332f4230 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -63,7 +63,7 @@ router.get('/apple-touch-icon.png', async ctx => {
});
});
-// ServiceWroker
+// ServiceWorker
router.get(/^\/sw\.(.+?)\.js$/, async ctx => {
await send(ctx, `/assets/sw.${ctx.params[0]}.js`, {
root: client
diff --git a/src/server/web/views/note.pug b/src/server/web/views/note.pug
index 22f1834059..234ecabe22 100644
--- a/src/server/web/views/note.pug
+++ b/src/server/web/views/note.pug
@@ -6,7 +6,7 @@ block vars
- const url = `${config.url}/notes/${note.id}`;
block title
- = `${title} | Misskey`
+ = `${title} | ${config.name}`
block desc
meta(name='description' content= summary)
@@ -23,3 +23,6 @@ block meta
link(rel='prev' href=`${config.url}/notes/${note.prev}`)
if note.next
link(rel='next' href=`${config.url}/notes/${note.next}`)
+
+ if !user.host
+ link(rel='alternate' href=url type='application/activity+json')
diff --git a/src/server/web/views/user.pug b/src/server/web/views/user.pug
index b5ea2f6eb4..506a889d98 100644
--- a/src/server/web/views/user.pug
+++ b/src/server/web/views/user.pug
@@ -2,11 +2,11 @@ extends ../../../../src/client/app/base
block vars
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
- - const url = config.url + '/@' + (user.host ? `${user.username}@${user.host}` : user.username);
+ - const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`;
- const img = user.avatarId ? `${config.drive_url}/${user.avatarId}` : null;
block title
- = `${title} | Misskey`
+ = `${title} | ${config.name}`
block desc
meta(name='description' content= user.description)
@@ -18,3 +18,10 @@ block meta
meta(property='og:description' content= user.description)
meta(property='og:url' content= url)
meta(property='og:image' content= img)
+
+ if !user.host
+ link(rel='alternate' href=`${config.url}/users/${user._id}` type='application/activity+json')
+ if user.uri
+ link(rel='alternate' href=user.uri type='application/activity+json')
+ if user.url
+ link(rel='alternate' href=user.url type='text/html')