diff options
Diffstat (limited to 'src/server')
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') |