From a1b490afa756a71b9cef4afa424575bc223bc612 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 02:30:37 +0900 Subject: Post --> Note Closes #1411 --- .../api/endpoints/aggregation/notes/reaction.ts | 76 +++++ .../api/endpoints/aggregation/notes/reactions.ts | 72 ++++ .../api/endpoints/aggregation/notes/reply.ts | 75 +++++ .../api/endpoints/aggregation/notes/repost.ts | 75 +++++ src/server/api/endpoints/aggregation/posts.ts | 22 +- .../api/endpoints/aggregation/posts/reaction.ts | 76 ----- .../api/endpoints/aggregation/posts/reactions.ts | 72 ---- .../api/endpoints/aggregation/posts/reply.ts | 75 ----- .../api/endpoints/aggregation/posts/repost.ts | 75 ----- .../api/endpoints/aggregation/users/activity.ts | 20 +- src/server/api/endpoints/aggregation/users/post.ts | 22 +- .../api/endpoints/aggregation/users/reaction.ts | 2 +- src/server/api/endpoints/app/create.ts | 2 +- src/server/api/endpoints/app/name_id/available.ts | 2 +- src/server/api/endpoints/app/show.ts | 2 +- src/server/api/endpoints/auth/accept.ts | 2 +- src/server/api/endpoints/auth/session/generate.ts | 2 +- src/server/api/endpoints/auth/session/show.ts | 2 +- src/server/api/endpoints/auth/session/userkey.ts | 2 +- src/server/api/endpoints/channels/posts.ts | 10 +- src/server/api/endpoints/i/favorites.ts | 4 +- src/server/api/endpoints/i/pin.ts | 20 +- src/server/api/endpoints/meta.ts | 2 +- src/server/api/endpoints/notes/context.ts | 63 ++++ src/server/api/endpoints/notes/create.ts | 251 ++++++++++++++ src/server/api/endpoints/notes/favorites/create.ts | 48 +++ src/server/api/endpoints/notes/favorites/delete.ts | 46 +++ src/server/api/endpoints/notes/mentions.ts | 78 +++++ .../api/endpoints/notes/polls/recommendation.ts | 59 ++++ src/server/api/endpoints/notes/polls/vote.ts | 115 +++++++ src/server/api/endpoints/notes/reactions.ts | 57 ++++ src/server/api/endpoints/notes/reactions/create.ts | 47 +++ src/server/api/endpoints/notes/reactions/delete.ts | 60 ++++ src/server/api/endpoints/notes/replies.ts | 53 +++ src/server/api/endpoints/notes/reposts.ts | 73 +++++ src/server/api/endpoints/notes/search.ts | 364 +++++++++++++++++++++ src/server/api/endpoints/notes/show.ts | 32 ++ src/server/api/endpoints/notes/timeline.ts | 132 ++++++++ src/server/api/endpoints/notes/trend.ts | 79 +++++ src/server/api/endpoints/posts.ts | 18 +- src/server/api/endpoints/posts/context.ts | 63 ---- src/server/api/endpoints/posts/create.ts | 108 +++--- src/server/api/endpoints/posts/favorites/create.ts | 48 --- src/server/api/endpoints/posts/favorites/delete.ts | 46 --- src/server/api/endpoints/posts/mentions.ts | 78 ----- .../api/endpoints/posts/polls/recommendation.ts | 59 ---- src/server/api/endpoints/posts/polls/vote.ts | 115 ------- src/server/api/endpoints/posts/reactions.ts | 57 ---- src/server/api/endpoints/posts/reactions/create.ts | 47 --- src/server/api/endpoints/posts/reactions/delete.ts | 60 ---- src/server/api/endpoints/posts/replies.ts | 53 --- src/server/api/endpoints/posts/reposts.ts | 73 ----- src/server/api/endpoints/posts/search.ts | 364 --------------------- src/server/api/endpoints/posts/show.ts | 32 -- src/server/api/endpoints/posts/timeline.ts | 132 -------- src/server/api/endpoints/posts/trend.ts | 79 ----- src/server/api/endpoints/stats.ts | 12 +- .../users/get_frequently_replied_users.ts | 18 +- src/server/api/endpoints/users/posts.ts | 10 +- 59 files changed, 1996 insertions(+), 1745 deletions(-) create mode 100644 src/server/api/endpoints/aggregation/notes/reaction.ts create mode 100644 src/server/api/endpoints/aggregation/notes/reactions.ts create mode 100644 src/server/api/endpoints/aggregation/notes/reply.ts create mode 100644 src/server/api/endpoints/aggregation/notes/repost.ts delete mode 100644 src/server/api/endpoints/aggregation/posts/reaction.ts delete mode 100644 src/server/api/endpoints/aggregation/posts/reactions.ts delete mode 100644 src/server/api/endpoints/aggregation/posts/reply.ts delete mode 100644 src/server/api/endpoints/aggregation/posts/repost.ts create mode 100644 src/server/api/endpoints/notes/context.ts create mode 100644 src/server/api/endpoints/notes/create.ts create mode 100644 src/server/api/endpoints/notes/favorites/create.ts create mode 100644 src/server/api/endpoints/notes/favorites/delete.ts create mode 100644 src/server/api/endpoints/notes/mentions.ts create mode 100644 src/server/api/endpoints/notes/polls/recommendation.ts create mode 100644 src/server/api/endpoints/notes/polls/vote.ts create mode 100644 src/server/api/endpoints/notes/reactions.ts create mode 100644 src/server/api/endpoints/notes/reactions/create.ts create mode 100644 src/server/api/endpoints/notes/reactions/delete.ts create mode 100644 src/server/api/endpoints/notes/replies.ts create mode 100644 src/server/api/endpoints/notes/reposts.ts create mode 100644 src/server/api/endpoints/notes/search.ts create mode 100644 src/server/api/endpoints/notes/show.ts create mode 100644 src/server/api/endpoints/notes/timeline.ts create mode 100644 src/server/api/endpoints/notes/trend.ts delete mode 100644 src/server/api/endpoints/posts/context.ts delete mode 100644 src/server/api/endpoints/posts/favorites/create.ts delete mode 100644 src/server/api/endpoints/posts/favorites/delete.ts delete mode 100644 src/server/api/endpoints/posts/mentions.ts delete mode 100644 src/server/api/endpoints/posts/polls/recommendation.ts delete mode 100644 src/server/api/endpoints/posts/polls/vote.ts delete mode 100644 src/server/api/endpoints/posts/reactions.ts delete mode 100644 src/server/api/endpoints/posts/reactions/create.ts delete mode 100644 src/server/api/endpoints/posts/reactions/delete.ts delete mode 100644 src/server/api/endpoints/posts/replies.ts delete mode 100644 src/server/api/endpoints/posts/reposts.ts delete mode 100644 src/server/api/endpoints/posts/search.ts delete mode 100644 src/server/api/endpoints/posts/show.ts delete mode 100644 src/server/api/endpoints/posts/timeline.ts delete mode 100644 src/server/api/endpoints/posts/trend.ts (limited to 'src/server/api/endpoints') diff --git a/src/server/api/endpoints/aggregation/notes/reaction.ts b/src/server/api/endpoints/aggregation/notes/reaction.ts new file mode 100644 index 0000000000..586e8c2d85 --- /dev/null +++ b/src/server/api/endpoints/aggregation/notes/reaction.ts @@ -0,0 +1,76 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note from '../../../../../models/note'; +import Reaction from '../../../../../models/note-reaction'; + +/** + * Aggregate reaction of a note + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Lookup note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + const datas = await Reaction + .aggregate([ + { $match: { noteId: note._id } }, + { $project: { + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST + }}, + { $project: { + date: { + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } + } + }}, + { $group: { + _id: '$date', + count: { $sum: 1 } + }} + ]); + + datas.forEach(data => { + data.date = data._id; + delete data._id; + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + const day = new Date(new Date().setDate(new Date().getDate() - i)); + + const data = datas.filter(d => + d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() + )[0]; + + if (data) { + graph.push(data); + } else { + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: 0 + }); + } + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/notes/reactions.ts b/src/server/api/endpoints/aggregation/notes/reactions.ts new file mode 100644 index 0000000000..ff9491292e --- /dev/null +++ b/src/server/api/endpoints/aggregation/notes/reactions.ts @@ -0,0 +1,72 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note from '../../../../../models/note'; +import Reaction from '../../../../../models/note-reaction'; + +/** + * Aggregate reactions of a note + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Lookup note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); + + const reactions = await Reaction + .find({ + noteId: note._id, + $or: [ + { deletedAt: { $exists: false } }, + { deletedAt: { $gt: startTime } } + ] + }, { + sort: { + _id: -1 + }, + fields: { + _id: false, + noteId: false + } + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + let day = new Date(new Date().setDate(new Date().getDate() - i)); + day = new Date(day.setMilliseconds(999)); + day = new Date(day.setSeconds(59)); + day = new Date(day.setMinutes(59)); + day = new Date(day.setHours(23)); + // day = day.getTime(); + + const count = reactions.filter(r => + r.createdAt < day && (r.deletedAt == null || r.deletedAt > day) + ).length; + + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: count + }); + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/notes/reply.ts b/src/server/api/endpoints/aggregation/notes/reply.ts new file mode 100644 index 0000000000..42df95a9a5 --- /dev/null +++ b/src/server/api/endpoints/aggregation/notes/reply.ts @@ -0,0 +1,75 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note from '../../../../../models/note'; + +/** + * Aggregate reply of a note + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Lookup note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + const datas = await Note + .aggregate([ + { $match: { reply: note._id } }, + { $project: { + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST + }}, + { $project: { + date: { + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } + } + }}, + { $group: { + _id: '$date', + count: { $sum: 1 } + }} + ]); + + datas.forEach(data => { + data.date = data._id; + delete data._id; + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + const day = new Date(new Date().setDate(new Date().getDate() - i)); + + const data = datas.filter(d => + d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() + )[0]; + + if (data) { + graph.push(data); + } else { + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: 0 + }); + } + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/notes/repost.ts b/src/server/api/endpoints/aggregation/notes/repost.ts new file mode 100644 index 0000000000..feb3348a7e --- /dev/null +++ b/src/server/api/endpoints/aggregation/notes/repost.ts @@ -0,0 +1,75 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note from '../../../../../models/note'; + +/** + * Aggregate renote of a note + * + * @param {any} params + * @return {Promise} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Lookup note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + const datas = await Note + .aggregate([ + { $match: { renoteId: note._id } }, + { $project: { + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST + }}, + { $project: { + date: { + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } + } + }}, + { $group: { + _id: '$date', + count: { $sum: 1 } + }} + ]); + + datas.forEach(data => { + data.date = data._id; + delete data._id; + }); + + const graph = []; + + for (let i = 0; i < 30; i++) { + const day = new Date(new Date().setDate(new Date().getDate() - i)); + + const data = datas.filter(d => + d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() + )[0]; + + if (data) { + graph.push(data); + } else { + graph.push({ + date: { + year: day.getFullYear(), + month: day.getMonth() + 1, // In JavaScript, month is zero-based. + day: day.getDate() + }, + count: 0 + }); + } + } + + res(graph); +}); diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts index f4d401eda0..cc2a48b53d 100644 --- a/src/server/api/endpoints/aggregation/posts.ts +++ b/src/server/api/endpoints/aggregation/posts.ts @@ -2,10 +2,10 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../../../models/post'; +import Note from '../../../../models/note'; /** - * Aggregate posts + * Aggregate notes * * @param {any} params * @return {Promise} @@ -15,10 +15,10 @@ module.exports = params => new Promise(async (res, rej) => { const [limit = 365, limitErr] = $(params.limit).optional.number().range(1, 365).$; if (limitErr) return rej('invalid limit param'); - const datas = await Post + const datas = await Note .aggregate([ { $project: { - repostId: '$repostId', + renoteId: '$renoteId', replyId: '$replyId', createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, @@ -30,13 +30,13 @@ module.exports = params => new Promise(async (res, rej) => { }, type: { $cond: { - if: { $ne: ['$repostId', null] }, - then: 'repost', + if: { $ne: ['$renoteId', null] }, + then: 'renote', else: { $cond: { if: { $ne: ['$replyId', null] }, then: 'reply', - else: 'post' + else: 'note' } } } @@ -59,8 +59,8 @@ module.exports = params => new Promise(async (res, rej) => { data.date = data._id; delete data._id; - data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count; - data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count; + data.notes = (data.data.filter(x => x.type == 'note')[0] || { count: 0 }).count; + data.renotes = (data.data.filter(x => x.type == 'renote')[0] || { count: 0 }).count; data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count; delete data.data; @@ -79,8 +79,8 @@ module.exports = params => new Promise(async (res, rej) => { graph.push(data); } else { graph.push({ - posts: 0, - reposts: 0, + notes: 0, + renotes: 0, replies: 0 }); } diff --git a/src/server/api/endpoints/aggregation/posts/reaction.ts b/src/server/api/endpoints/aggregation/posts/reaction.ts deleted file mode 100644 index e622745337..0000000000 --- a/src/server/api/endpoints/aggregation/posts/reaction.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../../../models/post'; -import Reaction from '../../../../../models/post-reaction'; - -/** - * Aggregate reaction of a post - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const datas = await Reaction - .aggregate([ - { $match: { postId: post._id } }, - { $project: { - createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST - }}, - { $project: { - date: { - year: { $year: '$createdAt' }, - month: { $month: '$createdAt' }, - day: { $dayOfMonth: '$createdAt' } - } - }}, - { $group: { - _id: '$date', - count: { $sum: 1 } - }} - ]); - - datas.forEach(data => { - data.date = data._id; - delete data._id; - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - const day = new Date(new Date().setDate(new Date().getDate() - i)); - - const data = datas.filter(d => - d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() - )[0]; - - if (data) { - graph.push(data); - } else { - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: 0 - }); - } - } - - res(graph); -}); diff --git a/src/server/api/endpoints/aggregation/posts/reactions.ts b/src/server/api/endpoints/aggregation/posts/reactions.ts deleted file mode 100644 index 5f23e296fd..0000000000 --- a/src/server/api/endpoints/aggregation/posts/reactions.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../../../models/post'; -import Reaction from '../../../../../models/post-reaction'; - -/** - * Aggregate reactions of a post - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); - - const reactions = await Reaction - .find({ - postId: post._id, - $or: [ - { deletedAt: { $exists: false } }, - { deletedAt: { $gt: startTime } } - ] - }, { - sort: { - _id: -1 - }, - fields: { - _id: false, - postId: false - } - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - let day = new Date(new Date().setDate(new Date().getDate() - i)); - day = new Date(day.setMilliseconds(999)); - day = new Date(day.setSeconds(59)); - day = new Date(day.setMinutes(59)); - day = new Date(day.setHours(23)); - // day = day.getTime(); - - const count = reactions.filter(r => - r.createdAt < day && (r.deletedAt == null || r.deletedAt > day) - ).length; - - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: count - }); - } - - res(graph); -}); diff --git a/src/server/api/endpoints/aggregation/posts/reply.ts b/src/server/api/endpoints/aggregation/posts/reply.ts deleted file mode 100644 index c76191e86b..0000000000 --- a/src/server/api/endpoints/aggregation/posts/reply.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../../../models/post'; - -/** - * Aggregate reply of a post - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const datas = await Post - .aggregate([ - { $match: { reply: post._id } }, - { $project: { - createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST - }}, - { $project: { - date: { - year: { $year: '$createdAt' }, - month: { $month: '$createdAt' }, - day: { $dayOfMonth: '$createdAt' } - } - }}, - { $group: { - _id: '$date', - count: { $sum: 1 } - }} - ]); - - datas.forEach(data => { - data.date = data._id; - delete data._id; - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - const day = new Date(new Date().setDate(new Date().getDate() - i)); - - const data = datas.filter(d => - d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() - )[0]; - - if (data) { - graph.push(data); - } else { - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: 0 - }); - } - } - - res(graph); -}); diff --git a/src/server/api/endpoints/aggregation/posts/repost.ts b/src/server/api/endpoints/aggregation/posts/repost.ts deleted file mode 100644 index a203605ebf..0000000000 --- a/src/server/api/endpoints/aggregation/posts/repost.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../../../models/post'; - -/** - * Aggregate repost of a post - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const datas = await Post - .aggregate([ - { $match: { repostId: post._id } }, - { $project: { - createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST - }}, - { $project: { - date: { - year: { $year: '$createdAt' }, - month: { $month: '$createdAt' }, - day: { $dayOfMonth: '$createdAt' } - } - }}, - { $group: { - _id: '$date', - count: { $sum: 1 } - }} - ]); - - datas.forEach(data => { - data.date = data._id; - delete data._id; - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - const day = new Date(new Date().setDate(new Date().getDate() - i)); - - const data = datas.filter(d => - d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() - )[0]; - - if (data) { - graph.push(data); - } else { - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: 0 - }); - } - } - - res(graph); -}); diff --git a/src/server/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts index cef0072296..318cce77a5 100644 --- a/src/server/api/endpoints/aggregation/users/activity.ts +++ b/src/server/api/endpoints/aggregation/users/activity.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import User from '../../../../../models/user'; -import Post from '../../../../../models/post'; +import Note from '../../../../../models/note'; // TODO: likeやfollowも集計 @@ -35,11 +35,11 @@ module.exports = (params) => new Promise(async (res, rej) => { return rej('user not found'); } - const datas = await Post + const datas = await Note .aggregate([ { $match: { userId: user._id } }, { $project: { - repostId: '$repostId', + renoteId: '$renoteId', replyId: '$replyId', createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, @@ -51,13 +51,13 @@ module.exports = (params) => new Promise(async (res, rej) => { }, type: { $cond: { - if: { $ne: ['$repostId', null] }, - then: 'repost', + if: { $ne: ['$renoteId', null] }, + then: 'renote', else: { $cond: { if: { $ne: ['$replyId', null] }, then: 'reply', - else: 'post' + else: 'note' } } } @@ -80,8 +80,8 @@ module.exports = (params) => new Promise(async (res, rej) => { data.date = data._id; delete data._id; - data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count; - data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count; + data.notes = (data.data.filter(x => x.type == 'note')[0] || { count: 0 }).count; + data.renotes = (data.data.filter(x => x.type == 'renote')[0] || { count: 0 }).count; data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count; delete data.data; @@ -105,8 +105,8 @@ module.exports = (params) => new Promise(async (res, rej) => { month: day.getMonth() + 1, // In JavaScript, month is zero-based. day: day.getDate() }, - posts: 0, - reposts: 0, + notes: 0, + renotes: 0, replies: 0 }); } diff --git a/src/server/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts index 13617cf639..e6170d83e2 100644 --- a/src/server/api/endpoints/aggregation/users/post.ts +++ b/src/server/api/endpoints/aggregation/users/post.ts @@ -3,10 +3,10 @@ */ import $ from 'cafy'; import User from '../../../../../models/user'; -import Post from '../../../../../models/post'; +import Note from '../../../../../models/note'; /** - * Aggregate post of a user + * Aggregate note of a user * * @param {any} params * @return {Promise} @@ -29,11 +29,11 @@ module.exports = (params) => new Promise(async (res, rej) => { return rej('user not found'); } - const datas = await Post + const datas = await Note .aggregate([ { $match: { userId: user._id } }, { $project: { - repostId: '$repostId', + renoteId: '$renoteId', replyId: '$replyId', createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, @@ -45,13 +45,13 @@ module.exports = (params) => new Promise(async (res, rej) => { }, type: { $cond: { - if: { $ne: ['$repostId', null] }, - then: 'repost', + if: { $ne: ['$renoteId', null] }, + then: 'renote', else: { $cond: { if: { $ne: ['$replyId', null] }, then: 'reply', - else: 'post' + else: 'note' } } } @@ -74,8 +74,8 @@ module.exports = (params) => new Promise(async (res, rej) => { data.date = data._id; delete data._id; - data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count; - data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count; + data.notes = (data.data.filter(x => x.type == 'note')[0] || { count: 0 }).count; + data.renotes = (data.data.filter(x => x.type == 'renote')[0] || { count: 0 }).count; data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count; delete data.data; @@ -99,8 +99,8 @@ module.exports = (params) => new Promise(async (res, rej) => { month: day.getMonth() + 1, // In JavaScript, month is zero-based. day: day.getDate() }, - posts: 0, - reposts: 0, + notes: 0, + renotes: 0, replies: 0 }); } diff --git a/src/server/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts index 0c42ba3360..881c7ea693 100644 --- a/src/server/api/endpoints/aggregation/users/reaction.ts +++ b/src/server/api/endpoints/aggregation/users/reaction.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import User from '../../../../../models/user'; -import Reaction from '../../../../../models/post-reaction'; +import Reaction from '../../../../../models/note-reaction'; /** * Aggregate reaction of a user diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts index f2033d33f6..4a55d33f2d 100644 --- a/src/server/api/endpoints/app/create.ts +++ b/src/server/api/endpoints/app/create.ts @@ -8,7 +8,7 @@ import App, { isValidNameId, pack } from '../../../../models/app'; /** * @swagger * /app/create: - * post: + * note: * summary: Create an application * parameters: * - $ref: "#/parameters/AccessToken" diff --git a/src/server/api/endpoints/app/name_id/available.ts b/src/server/api/endpoints/app/name_id/available.ts index 93b33cfa20..ec2d692412 100644 --- a/src/server/api/endpoints/app/name_id/available.ts +++ b/src/server/api/endpoints/app/name_id/available.ts @@ -8,7 +8,7 @@ import { isValidNameId } from '../../../../../models/app'; /** * @swagger * /app/nameId/available: - * post: + * note: * summary: Check available nameId on creation an application * parameters: * - diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts index 7c8d2881d5..3a3c25f47c 100644 --- a/src/server/api/endpoints/app/show.ts +++ b/src/server/api/endpoints/app/show.ts @@ -7,7 +7,7 @@ import App, { pack } from '../../../../models/app'; /** * @swagger * /app/show: - * post: + * note: * summary: Show an application's information * description: Require appId or nameId * parameters: diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts index aeabac2db8..b6297d663d 100644 --- a/src/server/api/endpoints/auth/accept.ts +++ b/src/server/api/endpoints/auth/accept.ts @@ -11,7 +11,7 @@ import AccessToken from '../../../../models/access-token'; /** * @swagger * /auth/accept: - * post: + * note: * summary: Accept a session * parameters: * - $ref: "#/parameters/NativeToken" diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts index 9857b31d87..7c475dbe26 100644 --- a/src/server/api/endpoints/auth/session/generate.ts +++ b/src/server/api/endpoints/auth/session/generate.ts @@ -10,7 +10,7 @@ import config from '../../../../../config'; /** * @swagger * /auth/session/generate: - * post: + * note: * summary: Generate a session * parameters: * - diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts index f473d73973..f7f0b087b7 100644 --- a/src/server/api/endpoints/auth/session/show.ts +++ b/src/server/api/endpoints/auth/session/show.ts @@ -7,7 +7,7 @@ import AuthSess, { pack } from '../../../../../models/auth-session'; /** * @swagger * /auth/session/show: - * post: + * note: * summary: Show a session information * parameters: * - diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts index 7dbb5ea6e8..ddb67cb451 100644 --- a/src/server/api/endpoints/auth/session/userkey.ts +++ b/src/server/api/endpoints/auth/session/userkey.ts @@ -10,7 +10,7 @@ import { pack } from '../../../../../models/user'; /** * @swagger * /auth/session/userkey: - * post: + * note: * summary: Get an access token(userkey) * parameters: * - diff --git a/src/server/api/endpoints/channels/posts.ts b/src/server/api/endpoints/channels/posts.ts index e48f96da7e..d636aa0d10 100644 --- a/src/server/api/endpoints/channels/posts.ts +++ b/src/server/api/endpoints/channels/posts.ts @@ -3,10 +3,10 @@ */ import $ from 'cafy'; import { default as Channel, IChannel } from '../../../../models/channel'; -import Post, { pack } from '../../../../models/post'; +import Note, { pack } from '../../../../models/note'; /** - * Show a posts of a channel + * Show a notes of a channel * * @param {any} params * @param {any} user @@ -65,14 +65,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { //#endregion Construct query // Issue query - const posts = await Post + const notes = await Note .find(query, { limit: limit, sort: sort }); // Serialize - res(await Promise.all(posts.map(async (post) => - await pack(post, user) + res(await Promise.all(notes.map(async (note) => + await pack(note, user) ))); }); diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts index 0b594e3180..b40f2b3887 100644 --- a/src/server/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import Favorite from '../../../../models/favorite'; -import { pack } from '../../../../models/post'; +import { pack } from '../../../../models/note'; /** * Get followers of a user @@ -39,6 +39,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(favorites.map(async favorite => - await pack(favorite.postId) + await pack(favorite.noteId) ))); }); diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts index 2a57579772..909a6fdbde 100644 --- a/src/server/api/endpoints/i/pin.ts +++ b/src/server/api/endpoints/i/pin.ts @@ -3,34 +3,34 @@ */ import $ from 'cafy'; import User from '../../../../models/user'; -import Post from '../../../../models/post'; +import Note from '../../../../models/note'; import { pack } from '../../../../models/user'; /** - * Pin post + * Pin note * * @param {any} params * @param {any} user * @return {Promise} */ module.exports = async (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); // Fetch pinee - const post = await Post.findOne({ - _id: postId, + const note = await Note.findOne({ + _id: noteId, userId: user._id }); - if (post === null) { - return rej('post not found'); + if (note === null) { + return rej('note not found'); } await User.update(user._id, { $set: { - pinnedPostId: post._id + pinnedNoteId: note._id } }); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 70ae7e99cc..8574362fc8 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -9,7 +9,7 @@ import Meta from '../../../models/meta'; /** * @swagger * /meta: - * post: + * note: * summary: Show the misskey's information * responses: * 200: diff --git a/src/server/api/endpoints/notes/context.ts b/src/server/api/endpoints/notes/context.ts new file mode 100644 index 0000000000..2caf742d26 --- /dev/null +++ b/src/server/api/endpoints/notes/context.ts @@ -0,0 +1,63 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note, { pack } from '../../../../models/note'; + +/** + * Show a context of a note + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Lookup note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + const context = []; + let i = 0; + + async function get(id) { + i++; + const p = await Note.findOne({ _id: id }); + + if (i > offset) { + context.push(p); + } + + if (context.length == limit) { + return; + } + + if (p.replyId) { + await get(p.replyId); + } + } + + if (note.replyId) { + await get(note.replyId); + } + + // Serialize + res(await Promise.all(context.map(async note => + await pack(note, user)))); +}); diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts new file mode 100644 index 0000000000..7e79912b1b --- /dev/null +++ b/src/server/api/endpoints/notes/create.ts @@ -0,0 +1,251 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import deepEqual = require('deep-equal'); +import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note'; +import { ILocalUser } from '../../../../models/user'; +import Channel, { IChannel } from '../../../../models/channel'; +import DriveFile from '../../../../models/drive-file'; +import create from '../../../../services/note/create'; +import { IApp } from '../../../../models/app'; + +/** + * Create a note + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise} + */ +module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => { + // Get 'visibility' parameter + const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).$; + if (visibilityErr) return rej('invalid visibility'); + + // Get 'text' parameter + const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; + if (textErr) return rej('invalid text'); + + // Get 'cw' parameter + const [cw, cwErr] = $(params.cw).optional.string().pipe(isValidCw).$; + if (cwErr) return rej('invalid cw'); + + // Get 'viaMobile' parameter + const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$; + if (viaMobileErr) return rej('invalid viaMobile'); + + // Get 'tags' parameter + const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$; + if (tagsErr) return rej('invalid tags'); + + // Get 'geo' parameter + const [geo, geoErr] = $(params.geo).optional.nullable.strict.object() + .have('coordinates', $().array().length(2) + .item(0, $().number().range(-180, 180)) + .item(1, $().number().range(-90, 90))) + .have('altitude', $().nullable.number()) + .have('accuracy', $().nullable.number()) + .have('altitudeAccuracy', $().nullable.number()) + .have('heading', $().nullable.number().range(0, 360)) + .have('speed', $().nullable.number()) + .$; + if (geoErr) return rej('invalid geo'); + + // Get 'mediaIds' parameter + const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$; + if (mediaIdsErr) return rej('invalid mediaIds'); + + let files = []; + if (mediaIds !== undefined) { + // Fetch files + // forEach だと途中でエラーなどがあっても return できないので + // 敢えて for を使っています。 + for (const mediaId of mediaIds) { + // Fetch file + // SELECT _id + const entity = await DriveFile.findOne({ + _id: mediaId, + 'metadata.userId': user._id + }); + + if (entity === null) { + return rej('file not found'); + } else { + files.push(entity); + } + } + } else { + files = null; + } + + // Get 'renoteId' parameter + const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$; + if (renoteIdErr) return rej('invalid renoteId'); + + let renote: INote = null; + let isQuote = false; + if (renoteId !== undefined) { + // Fetch renote to note + renote = await Note.findOne({ + _id: renoteId + }); + + if (renote == null) { + return rej('renoteee is not found'); + } else if (renote.renoteId && !renote.text && !renote.mediaIds) { + return rej('cannot renote to renote'); + } + + // Fetch recently note + const latestNote = await Note.findOne({ + userId: user._id + }, { + sort: { + _id: -1 + } + }); + + isQuote = text != null || files != null; + + // 直近と同じRenote対象かつ引用じゃなかったらエラー + if (latestNote && + latestNote.renoteId && + latestNote.renoteId.equals(renote._id) && + !isQuote) { + return rej('cannot renote same note that already reposted in your latest note'); + } + + // 直近がRenote対象かつ引用じゃなかったらエラー + if (latestNote && + latestNote._id.equals(renote._id) && + !isQuote) { + return rej('cannot renote your latest note'); + } + } + + // Get 'replyId' parameter + const [replyId, replyIdErr] = $(params.replyId).optional.id().$; + if (replyIdErr) return rej('invalid replyId'); + + let reply: INote = null; + if (replyId !== undefined) { + // Fetch reply + reply = await Note.findOne({ + _id: replyId + }); + + if (reply === null) { + return rej('in reply to note is not found'); + } + + // 返信対象が引用でないRenoteだったらエラー + if (reply.renoteId && !reply.text && !reply.mediaIds) { + return rej('cannot reply to renote'); + } + } + + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).optional.id().$; + if (channelIdErr) return rej('invalid channelId'); + + let channel: IChannel = null; + if (channelId !== undefined) { + // Fetch channel + channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + // 返信対象の投稿がこのチャンネルじゃなかったらダメ + if (reply && !channelId.equals(reply.channelId)) { + return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません'); + } + + // Renote対象の投稿がこのチャンネルじゃなかったらダメ + if (renote && !channelId.equals(renote.channelId)) { + return rej('チャンネル内部からチャンネル外部の投稿をRenoteすることはできません'); + } + + // 引用ではないRenoteはダメ + if (renote && !isQuote) { + return rej('チャンネル内部では引用ではないRenoteをすることはできません'); + } + } else { + // 返信対象の投稿がチャンネルへの投稿だったらダメ + if (reply && reply.channelId != null) { + return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません'); + } + + // Renote対象の投稿がチャンネルへの投稿だったらダメ + if (renote && renote.channelId != null) { + return rej('チャンネル外部からチャンネル内部の投稿をRenoteすることはできません'); + } + } + + // Get 'poll' parameter + const [poll, pollErr] = $(params.poll).optional.strict.object() + .have('choices', $().array('string') + .unique() + .range(2, 10) + .each(c => c.length > 0 && c.length < 50)) + .$; + if (pollErr) return rej('invalid poll'); + + if (poll) { + (poll as any).choices = (poll as any).choices.map((choice, i) => ({ + id: i, // IDを付与 + text: choice.trim(), + votes: 0 + })); + } + + // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー + if (text === undefined && files === null && renote === null && poll === undefined) { + return rej('text, mediaIds, renoteId or poll is required'); + } + + // 直近の投稿と重複してたらエラー + // TODO: 直近の投稿が一日前くらいなら重複とは見なさない + if (user.latestNote) { + if (deepEqual({ + text: user.latestNote.text, + reply: user.latestNote.replyId ? user.latestNote.replyId.toString() : null, + renote: user.latestNote.renoteId ? user.latestNote.renoteId.toString() : null, + mediaIds: (user.latestNote.mediaIds || []).map(id => id.toString()) + }, { + text: text, + reply: reply ? reply._id.toString() : null, + renote: renote ? renote._id.toString() : null, + mediaIds: (files || []).map(file => file._id.toString()) + })) { + return rej('duplicate'); + } + } + + // 投稿を作成 + const note = await create(user, { + createdAt: new Date(), + media: files, + poll: poll, + text: text, + reply, + renote, + cw: cw, + tags: tags, + app: app, + viaMobile: viaMobile, + visibility, + geo + }); + + const noteObj = await pack(note, user); + + // Reponse + res({ + createdNote: noteObj + }); +}); diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts new file mode 100644 index 0000000000..c8e7f52426 --- /dev/null +++ b/src/server/api/endpoints/notes/favorites/create.ts @@ -0,0 +1,48 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Favorite from '../../../../../models/favorite'; +import Note from '../../../../../models/note'; + +/** + * Favorite a note + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get favoritee + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // if already favorited + const exist = await Favorite.findOne({ + noteId: note._id, + userId: user._id + }); + + if (exist !== null) { + return rej('already favorited'); + } + + // Create favorite + await Favorite.insert({ + createdAt: new Date(), + noteId: note._id, + userId: user._id + }); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts new file mode 100644 index 0000000000..92aceb343b --- /dev/null +++ b/src/server/api/endpoints/notes/favorites/delete.ts @@ -0,0 +1,46 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Favorite from '../../../../../models/favorite'; +import Note from '../../../../../models/note'; + +/** + * Unfavorite a note + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get favoritee + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // if already favorited + const exist = await Favorite.findOne({ + noteId: note._id, + userId: user._id + }); + + if (exist === null) { + return rej('already not favorited'); + } + + // Delete favorite + await Favorite.remove({ + _id: exist._id + }); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts new file mode 100644 index 0000000000..c507acbaec --- /dev/null +++ b/src/server/api/endpoints/notes/mentions.ts @@ -0,0 +1,78 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note from '../../../../models/note'; +import getFriends from '../../common/get-friends'; +import { pack } from '../../../../models/note'; + +/** + * Get mentions of myself + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'following' parameter + const [following = false, followingError] = + $(params.following).optional.boolean().$; + if (followingError) return rej('invalid following param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); + } + + // Construct query + const query = { + mentions: user._id + } as any; + + const sort = { + _id: -1 + }; + + if (following) { + const followingIds = await getFriends(user._id); + + query.userId = { + $in: followingIds + }; + } + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const mentions = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(mentions.map(async mention => + await pack(mention, user) + ))); +}); diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts new file mode 100644 index 0000000000..cb530ea2cf --- /dev/null +++ b/src/server/api/endpoints/notes/polls/recommendation.ts @@ -0,0 +1,59 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Vote from '../../../../../models/poll-vote'; +import Note, { pack } from '../../../../../models/note'; + +/** + * Get recommended polls + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get votes + const votes = await Vote.find({ + userId: user._id + }, { + fields: { + _id: false, + noteId: true + } + }); + + const nin = votes && votes.length != 0 ? votes.map(v => v.noteId) : []; + + const notes = await Note + .find({ + _id: { + $nin: nin + }, + userId: { + $ne: user._id + }, + poll: { + $exists: true, + $ne: null + } + }, { + limit: limit, + skip: offset, + sort: { + _id: -1 + } + }); + + // Serialize + res(await Promise.all(notes.map(async note => + await pack(note, user, { detail: true })))); +}); diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts new file mode 100644 index 0000000000..0e27f87ee2 --- /dev/null +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -0,0 +1,115 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Vote from '../../../../../models/poll-vote'; +import Note from '../../../../../models/note'; +import Watching from '../../../../../models/note-watching'; +import watch from '../../../../../note/watch'; +import { publishNoteStream } from '../../../../../publishers/stream'; +import notify from '../../../../../publishers/notify'; + +/** + * Vote poll of a note + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get votee + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + if (note.poll == null) { + return rej('poll not found'); + } + + // Get 'choice' parameter + const [choice, choiceError] = + $(params.choice).number() + .pipe(c => note.poll.choices.some(x => x.id == c)) + .$; + if (choiceError) return rej('invalid choice param'); + + // if already voted + const exist = await Vote.findOne({ + noteId: note._id, + userId: user._id + }); + + if (exist !== null) { + return rej('already voted'); + } + + // Create vote + await Vote.insert({ + createdAt: new Date(), + noteId: note._id, + userId: user._id, + choice: choice + }); + + // Send response + res(); + + const inc = {}; + inc[`poll.choices.${findWithAttr(note.poll.choices, 'id', choice)}.votes`] = 1; + + // Increment votes count + await Note.update({ _id: note._id }, { + $inc: inc + }); + + publishNoteStream(note._id, 'poll_voted'); + + // Notify + notify(note.userId, user._id, 'poll_vote', { + noteId: note._id, + choice: choice + }); + + // Fetch watchers + Watching + .find({ + noteId: note._id, + userId: { $ne: user._id }, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }, { + fields: { + userId: true + } + }) + .then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, 'poll_vote', { + noteId: note._id, + choice: choice + }); + }); + }); + + // この投稿をWatchする + if (user.account.settings.autoWatch !== false) { + watch(user._id, note); + } +}); + +function findWithAttr(array, attr, value) { + for (let i = 0; i < array.length; i += 1) { + if (array[i][attr] === value) { + return i; + } + } + return -1; +} diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts new file mode 100644 index 0000000000..bbff97bb0a --- /dev/null +++ b/src/server/api/endpoints/notes/reactions.ts @@ -0,0 +1,57 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note from '../../../../models/note'; +import Reaction, { pack } from '../../../../models/note-reaction'; + +/** + * Show reactions of a note + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'sort' parameter + const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$; + if (sortError) return rej('invalid sort param'); + + // Lookup note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // Issue query + const reactions = await Reaction + .find({ + noteId: note._id, + deletedAt: { $exists: false } + }, { + limit: limit, + skip: offset, + sort: { + _id: sort == 'asc' ? 1 : -1 + } + }); + + // Serialize + res(await Promise.all(reactions.map(async reaction => + await pack(reaction, user)))); +}); diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts new file mode 100644 index 0000000000..ffb7bcc35b --- /dev/null +++ b/src/server/api/endpoints/notes/reactions/create.ts @@ -0,0 +1,47 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Reaction from '../../../../../models/note-reaction'; +import Note from '../../../../../models/note'; +import create from '../../../../../services/note/reaction/create'; + +/** + * React to a note + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get 'reaction' parameter + const [reaction, reactionErr] = $(params.reaction).string().or([ + 'like', + 'love', + 'laugh', + 'hmm', + 'surprise', + 'congrats', + 'angry', + 'confused', + 'pudding' + ]).$; + if (reactionErr) return rej('invalid reaction param'); + + // Fetch reactee + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + try { + await create(user, note, reaction); + } catch (e) { + rej(e); + } + + res(); +}); diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts new file mode 100644 index 0000000000..b5d738b8ff --- /dev/null +++ b/src/server/api/endpoints/notes/reactions/delete.ts @@ -0,0 +1,60 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Reaction from '../../../../../models/note-reaction'; +import Note from '../../../../../models/note'; +// import event from '../../../publishers/stream'; + +/** + * Unreact to a note + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Fetch unreactee + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // if already unreacted + const exist = await Reaction.findOne({ + noteId: note._id, + userId: user._id, + deletedAt: { $exists: false } + }); + + if (exist === null) { + return rej('never reacted'); + } + + // Delete reaction + await Reaction.update({ + _id: exist._id + }, { + $set: { + deletedAt: new Date() + } + }); + + // Send response + res(); + + const dec = {}; + dec[`reactionCounts.${exist.reaction}`] = -1; + + // Decrement reactions count + Note.update({ _id: note._id }, { + $inc: dec + }); +}); diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts new file mode 100644 index 0000000000..88d9ff329a --- /dev/null +++ b/src/server/api/endpoints/notes/replies.ts @@ -0,0 +1,53 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note, { pack } from '../../../../models/note'; + +/** + * Show a replies of a note + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'sort' parameter + const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$; + if (sortError) return rej('invalid sort param'); + + // Lookup note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // Issue query + const replies = await Note + .find({ replyId: note._id }, { + limit: limit, + skip: offset, + sort: { + _id: sort == 'asc' ? 1 : -1 + } + }); + + // Serialize + res(await Promise.all(replies.map(async note => + await pack(note, user)))); +}); diff --git a/src/server/api/endpoints/notes/reposts.ts b/src/server/api/endpoints/notes/reposts.ts new file mode 100644 index 0000000000..9dfc2c3cb5 --- /dev/null +++ b/src/server/api/endpoints/notes/reposts.ts @@ -0,0 +1,73 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note, { pack } from '../../../../models/note'; + +/** + * Show a renotes of a note + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); + } + + // Lookup note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // Construct query + const sort = { + _id: -1 + }; + const query = { + renoteId: note._id + } as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const renotes = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(renotes.map(async note => + await pack(note, user)))); +}); diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts new file mode 100644 index 0000000000..bfa17b000e --- /dev/null +++ b/src/server/api/endpoints/notes/search.ts @@ -0,0 +1,364 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +const escapeRegexp = require('escape-regexp'); +import Note from '../../../../models/note'; +import User from '../../../../models/user'; +import Mute from '../../../../models/mute'; +import getFriends from '../../common/get-friends'; +import { pack } from '../../../../models/note'; + +/** + * Search a note + * + * @param {any} params + * @param {any} me + * @return {Promise} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'text' parameter + const [text, textError] = $(params.text).optional.string().$; + if (textError) return rej('invalid text param'); + + // Get 'includeUserIds' parameter + const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array('id').$; + if (includeUserIdsErr) return rej('invalid includeUserIds param'); + + // Get 'excludeUserIds' parameter + const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array('id').$; + if (excludeUserIdsErr) return rej('invalid excludeUserIds param'); + + // Get 'includeUserUsernames' parameter + const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array('string').$; + if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param'); + + // Get 'excludeUserUsernames' parameter + const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array('string').$; + if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param'); + + // Get 'following' parameter + const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$; + if (followingErr) return rej('invalid following param'); + + // Get 'mute' parameter + const [mute = 'mute_all', muteErr] = $(params.mute).optional.string().$; + if (muteErr) return rej('invalid mute param'); + + // Get 'reply' parameter + const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$; + if (replyErr) return rej('invalid reply param'); + + // Get 'renote' parameter + const [renote = null, renoteErr] = $(params.renote).optional.nullable.boolean().$; + if (renoteErr) return rej('invalid renote param'); + + // Get 'media' parameter + const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$; + if (mediaErr) return rej('invalid media param'); + + // Get 'poll' parameter + const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$; + if (pollErr) return rej('invalid poll param'); + + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; + + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + if (untilDateErr) throw 'invalid untilDate param'; + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$; + if (limitErr) return rej('invalid limit param'); + + let includeUsers = includeUserIds; + if (includeUserUsernames != null) { + const ids = (await Promise.all(includeUserUsernames.map(async (username) => { + const _user = await User.findOne({ + usernameLower: username.toLowerCase() + }); + return _user ? _user._id : null; + }))).filter(id => id != null); + includeUsers = includeUsers.concat(ids); + } + + let excludeUsers = excludeUserIds; + if (excludeUserUsernames != null) { + const ids = (await Promise.all(excludeUserUsernames.map(async (username) => { + const _user = await User.findOne({ + usernameLower: username.toLowerCase() + }); + return _user ? _user._id : null; + }))).filter(id => id != null); + excludeUsers = excludeUsers.concat(ids); + } + + search(res, rej, me, text, includeUsers, excludeUsers, following, + mute, reply, renote, media, poll, sinceDate, untilDate, offset, limit); +}); + +async function search( + res, rej, me, text, includeUserIds, excludeUserIds, following, + mute, reply, renote, media, poll, sinceDate, untilDate, offset, max) { + + let q: any = { + $and: [] + }; + + const push = x => q.$and.push(x); + + if (text) { + // 完全一致検索 + if (/"""(.+?)"""/.test(text)) { + const x = text.match(/"""(.+?)"""/)[1]; + push({ + text: x + }); + } else { + const tags = text.split(' ').filter(x => x[0] == '#'); + if (tags) { + push({ + $and: tags.map(x => ({ + tags: x + })) + }); + } + + push({ + $and: text.split(' ').map(x => ({ + // キーワードが-で始まる場合そのキーワードを除外する + text: x[0] == '-' ? { + $not: new RegExp(escapeRegexp(x.substr(1))) + } : new RegExp(escapeRegexp(x)) + })) + }); + } + } + + if (includeUserIds && includeUserIds.length != 0) { + push({ + userId: { + $in: includeUserIds + } + }); + } else if (excludeUserIds && excludeUserIds.length != 0) { + push({ + userId: { + $nin: excludeUserIds + } + }); + } + + if (following != null && me != null) { + const ids = await getFriends(me._id, false); + push({ + userId: following ? { + $in: ids + } : { + $nin: ids.concat(me._id) + } + }); + } + + if (me != null) { + const mutes = await Mute.find({ + muterId: me._id, + deletedAt: { $exists: false } + }); + const mutedUserIds = mutes.map(m => m.muteeId); + + switch (mute) { + case 'mute_all': + push({ + userId: { + $nin: mutedUserIds + }, + '_reply.userId': { + $nin: mutedUserIds + }, + '_renote.userId': { + $nin: mutedUserIds + } + }); + break; + case 'mute_related': + push({ + '_reply.userId': { + $nin: mutedUserIds + }, + '_renote.userId': { + $nin: mutedUserIds + } + }); + break; + case 'mute_direct': + push({ + userId: { + $nin: mutedUserIds + } + }); + break; + case 'direct_only': + push({ + userId: { + $in: mutedUserIds + } + }); + break; + case 'related_only': + push({ + $or: [{ + '_reply.userId': { + $in: mutedUserIds + } + }, { + '_renote.userId': { + $in: mutedUserIds + } + }] + }); + break; + case 'all_only': + push({ + $or: [{ + userId: { + $in: mutedUserIds + } + }, { + '_reply.userId': { + $in: mutedUserIds + } + }, { + '_renote.userId': { + $in: mutedUserIds + } + }] + }); + break; + } + } + + if (reply != null) { + if (reply) { + push({ + replyId: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + replyId: { + $exists: false + } + }, { + replyId: null + }] + }); + } + } + + if (renote != null) { + if (renote) { + push({ + renoteId: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + renoteId: { + $exists: false + } + }, { + renoteId: null + }] + }); + } + } + + if (media != null) { + if (media) { + push({ + mediaIds: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + mediaIds: { + $exists: false + } + }, { + mediaIds: null + }] + }); + } + } + + if (poll != null) { + if (poll) { + push({ + poll: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + poll: { + $exists: false + } + }, { + poll: null + }] + }); + } + } + + if (sinceDate) { + push({ + createdAt: { + $gt: new Date(sinceDate) + } + }); + } + + if (untilDate) { + push({ + createdAt: { + $lt: new Date(untilDate) + } + }); + } + + if (q.$and.length == 0) { + q = {}; + } + + // Search notes + const notes = await Note + .find(q, { + sort: { + _id: -1 + }, + limit: max, + skip: offset + }); + + // Serialize + res(await Promise.all(notes.map(async note => + await pack(note, me)))); +} diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts new file mode 100644 index 0000000000..67cdc3038b --- /dev/null +++ b/src/server/api/endpoints/notes/show.ts @@ -0,0 +1,32 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note, { pack } from '../../../../models/note'; + +/** + * Show a note + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // Serialize + res(await pack(note, user, { + detail: true + })); +}); diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts new file mode 100644 index 0000000000..5263cfb2aa --- /dev/null +++ b/src/server/api/endpoints/notes/timeline.ts @@ -0,0 +1,132 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import rap from '@prezzemolo/rap'; +import Note from '../../../../models/note'; +import Mute from '../../../../models/mute'; +import ChannelWatching from '../../../../models/channel-watching'; +import getFriends from '../../common/get-friends'; +import { pack } from '../../../../models/note'; + +/** + * Get timeline of myself + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise} + */ +module.exports = async (params, user, app) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) throw 'invalid limit param'; + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) throw 'invalid sinceId param'; + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) throw 'invalid untilId param'; + + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; + + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + if (untilDateErr) throw 'invalid untilDate param'; + + // Check if only one of sinceId, untilId, sinceDate, untilDate specified + if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; + } + + const { followingIds, watchingChannelIds, mutedUserIds } = await rap({ + // ID list of the user itself and other users who the user follows + followingIds: getFriends(user._id), + + // Watchしているチャンネルを取得 + watchingChannelIds: ChannelWatching.find({ + userId: user._id, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }).then(watches => watches.map(w => w.channelId)), + + // ミュートしているユーザーを取得 + mutedUserIds: Mute.find({ + muterId: user._id, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }).then(ms => ms.map(m => m.muteeId)) + }); + + //#region Construct query + const sort = { + _id: -1 + }; + + const query = { + $or: [{ + // フォローしている人のタイムラインへの投稿 + userId: { + $in: followingIds + }, + // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る + $or: [{ + channelId: { + $exists: false + } + }, { + channelId: null + }] + }, { + // Watchしているチャンネルへの投稿 + channelId: { + $in: watchingChannelIds + } + }], + // mute + userId: { + $nin: mutedUserIds + }, + '_reply.userId': { + $nin: mutedUserIds + }, + '_renote.userId': { + $nin: mutedUserIds + }, + } as any; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } else if (sinceDate) { + sort._id = 1; + query.createdAt = { + $gt: new Date(sinceDate) + }; + } else if (untilDate) { + query.createdAt = { + $lt: new Date(untilDate) + }; + } + //#endregion + + // Issue query + const timeline = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + return await Promise.all(timeline.map(note => pack(note, user))); +}; diff --git a/src/server/api/endpoints/notes/trend.ts b/src/server/api/endpoints/notes/trend.ts new file mode 100644 index 0000000000..48ecd5b843 --- /dev/null +++ b/src/server/api/endpoints/notes/trend.ts @@ -0,0 +1,79 @@ +/** + * Module dependencies + */ +const ms = require('ms'); +import $ from 'cafy'; +import Note, { pack } from '../../../../models/note'; + +/** + * Get trend notes + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'reply' parameter + const [reply, replyErr] = $(params.reply).optional.boolean().$; + if (replyErr) return rej('invalid reply param'); + + // Get 'renote' parameter + const [renote, renoteErr] = $(params.renote).optional.boolean().$; + if (renoteErr) return rej('invalid renote param'); + + // Get 'media' parameter + const [media, mediaErr] = $(params.media).optional.boolean().$; + if (mediaErr) return rej('invalid media param'); + + // Get 'poll' parameter + const [poll, pollErr] = $(params.poll).optional.boolean().$; + if (pollErr) return rej('invalid poll param'); + + const query = { + createdAt: { + $gte: new Date(Date.now() - ms('1days')) + }, + renoteCount: { + $gt: 0 + } + } as any; + + if (reply != undefined) { + query.replyId = reply ? { $exists: true, $ne: null } : null; + } + + if (renote != undefined) { + query.renoteId = renote ? { $exists: true, $ne: null } : null; + } + + if (media != undefined) { + query.mediaIds = media ? { $exists: true, $ne: null } : null; + } + + if (poll != undefined) { + query.poll = poll ? { $exists: true, $ne: null } : null; + } + + // Issue query + const notes = await Note + .find(query, { + limit: limit, + skip: offset, + sort: { + renoteCount: -1, + _id: -1 + } + }); + + // Serialize + res(await Promise.all(notes.map(async note => + await pack(note, user, { detail: true })))); +}); diff --git a/src/server/api/endpoints/posts.ts b/src/server/api/endpoints/posts.ts index 7af8cff671..3e3b67a66c 100644 --- a/src/server/api/endpoints/posts.ts +++ b/src/server/api/endpoints/posts.ts @@ -2,10 +2,10 @@ * Module dependencies */ import $ from 'cafy'; -import Post, { pack } from '../../../models/post'; +import Note, { pack } from '../../../models/note'; /** - * Lists all posts + * Lists all notes * * @param {any} params * @return {Promise} @@ -15,9 +15,9 @@ module.exports = (params) => new Promise(async (res, rej) => { const [reply, replyErr] = $(params.reply).optional.boolean().$; if (replyErr) return rej('invalid reply param'); - // Get 'repost' parameter - const [repost, repostErr] = $(params.repost).optional.boolean().$; - if (repostErr) return rej('invalid repost param'); + // Get 'renote' parameter + const [renote, renoteErr] = $(params.renote).optional.boolean().$; + if (renoteErr) return rej('invalid renote param'); // Get 'media' parameter const [media, mediaErr] = $(params.media).optional.boolean().$; @@ -68,8 +68,8 @@ module.exports = (params) => new Promise(async (res, rej) => { query.replyId = reply ? { $exists: true, $ne: null } : null; } - if (repost != undefined) { - query.repostId = repost ? { $exists: true, $ne: null } : null; + if (renote != undefined) { + query.renoteId = renote ? { $exists: true, $ne: null } : null; } if (media != undefined) { @@ -86,12 +86,12 @@ module.exports = (params) => new Promise(async (res, rej) => { //} // Issue query - const posts = await Post + const notes = await Note .find(query, { limit: limit, sort: sort }); // Serialize - res(await Promise.all(posts.map(async post => await pack(post)))); + res(await Promise.all(notes.map(async note => await pack(note)))); }); diff --git a/src/server/api/endpoints/posts/context.ts b/src/server/api/endpoints/posts/context.ts deleted file mode 100644 index 7abb045a49..0000000000 --- a/src/server/api/endpoints/posts/context.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post, { pack } from '../../../../models/post'; - -/** - * Show a context of a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'offset' parameter - const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; - if (offsetErr) return rej('invalid offset param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const context = []; - let i = 0; - - async function get(id) { - i++; - const p = await Post.findOne({ _id: id }); - - if (i > offset) { - context.push(p); - } - - if (context.length == limit) { - return; - } - - if (p.replyId) { - await get(p.replyId); - } - } - - if (post.replyId) { - await get(post.replyId); - } - - // Serialize - res(await Promise.all(context.map(async post => - await pack(post, user)))); -}); diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts index 003a892bc0..7e79912b1b 100644 --- a/src/server/api/endpoints/posts/create.ts +++ b/src/server/api/endpoints/posts/create.ts @@ -3,15 +3,15 @@ */ import $ from 'cafy'; import deepEqual = require('deep-equal'); -import Post, { IPost, isValidText, isValidCw, pack } from '../../../../models/post'; +import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; import Channel, { IChannel } from '../../../../models/channel'; import DriveFile from '../../../../models/drive-file'; -import create from '../../../../services/post/create'; +import create from '../../../../services/note/create'; import { IApp } from '../../../../models/app'; /** - * Create a post + * Create a note * * @param {any} params * @param {any} user @@ -79,26 +79,26 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res files = null; } - // Get 'repostId' parameter - const [repostId, repostIdErr] = $(params.repostId).optional.id().$; - if (repostIdErr) return rej('invalid repostId'); + // Get 'renoteId' parameter + const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$; + if (renoteIdErr) return rej('invalid renoteId'); - let repost: IPost = null; + let renote: INote = null; let isQuote = false; - if (repostId !== undefined) { - // Fetch repost to post - repost = await Post.findOne({ - _id: repostId + if (renoteId !== undefined) { + // Fetch renote to note + renote = await Note.findOne({ + _id: renoteId }); - if (repost == null) { - return rej('repostee is not found'); - } else if (repost.repostId && !repost.text && !repost.mediaIds) { - return rej('cannot repost to repost'); + if (renote == null) { + return rej('renoteee is not found'); + } else if (renote.renoteId && !renote.text && !renote.mediaIds) { + return rej('cannot renote to renote'); } - // Fetch recently post - const latestPost = await Post.findOne({ + // Fetch recently note + const latestNote = await Note.findOne({ userId: user._id }, { sort: { @@ -108,19 +108,19 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res isQuote = text != null || files != null; - // 直近と同じRepost対象かつ引用じゃなかったらエラー - if (latestPost && - latestPost.repostId && - latestPost.repostId.equals(repost._id) && + // 直近と同じRenote対象かつ引用じゃなかったらエラー + if (latestNote && + latestNote.renoteId && + latestNote.renoteId.equals(renote._id) && !isQuote) { - return rej('cannot repost same post that already reposted in your latest post'); + return rej('cannot renote same note that already reposted in your latest note'); } - // 直近がRepost対象かつ引用じゃなかったらエラー - if (latestPost && - latestPost._id.equals(repost._id) && + // 直近がRenote対象かつ引用じゃなかったらエラー + if (latestNote && + latestNote._id.equals(renote._id) && !isQuote) { - return rej('cannot repost your latest post'); + return rej('cannot renote your latest note'); } } @@ -128,20 +128,20 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res const [replyId, replyIdErr] = $(params.replyId).optional.id().$; if (replyIdErr) return rej('invalid replyId'); - let reply: IPost = null; + let reply: INote = null; if (replyId !== undefined) { // Fetch reply - reply = await Post.findOne({ + reply = await Note.findOne({ _id: replyId }); if (reply === null) { - return rej('in reply to post is not found'); + return rej('in reply to note is not found'); } - // 返信対象が引用でないRepostだったらエラー - if (reply.repostId && !reply.text && !reply.mediaIds) { - return rej('cannot reply to repost'); + // 返信対象が引用でないRenoteだったらエラー + if (reply.renoteId && !reply.text && !reply.mediaIds) { + return rej('cannot reply to renote'); } } @@ -165,14 +165,14 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません'); } - // Repost対象の投稿がこのチャンネルじゃなかったらダメ - if (repost && !channelId.equals(repost.channelId)) { - return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません'); + // Renote対象の投稿がこのチャンネルじゃなかったらダメ + if (renote && !channelId.equals(renote.channelId)) { + return rej('チャンネル内部からチャンネル外部の投稿をRenoteすることはできません'); } - // 引用ではないRepostはダメ - if (repost && !isQuote) { - return rej('チャンネル内部では引用ではないRepostをすることはできません'); + // 引用ではないRenoteはダメ + if (renote && !isQuote) { + return rej('チャンネル内部では引用ではないRenoteをすることはできません'); } } else { // 返信対象の投稿がチャンネルへの投稿だったらダメ @@ -180,9 +180,9 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません'); } - // Repost対象の投稿がチャンネルへの投稿だったらダメ - if (repost && repost.channelId != null) { - return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません'); + // Renote対象の投稿がチャンネルへの投稿だったらダメ + if (renote && renote.channelId != null) { + return rej('チャンネル外部からチャンネル内部の投稿をRenoteすることはできません'); } } @@ -203,23 +203,23 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res })); } - // テキストが無いかつ添付ファイルが無いかつRepostも無いかつ投票も無かったらエラー - if (text === undefined && files === null && repost === null && poll === undefined) { - return rej('text, mediaIds, repostId or poll is required'); + // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー + if (text === undefined && files === null && renote === null && poll === undefined) { + return rej('text, mediaIds, renoteId or poll is required'); } // 直近の投稿と重複してたらエラー // TODO: 直近の投稿が一日前くらいなら重複とは見なさない - if (user.latestPost) { + if (user.latestNote) { if (deepEqual({ - text: user.latestPost.text, - reply: user.latestPost.replyId ? user.latestPost.replyId.toString() : null, - repost: user.latestPost.repostId ? user.latestPost.repostId.toString() : null, - mediaIds: (user.latestPost.mediaIds || []).map(id => id.toString()) + text: user.latestNote.text, + reply: user.latestNote.replyId ? user.latestNote.replyId.toString() : null, + renote: user.latestNote.renoteId ? user.latestNote.renoteId.toString() : null, + mediaIds: (user.latestNote.mediaIds || []).map(id => id.toString()) }, { text: text, reply: reply ? reply._id.toString() : null, - repost: repost ? repost._id.toString() : null, + renote: renote ? renote._id.toString() : null, mediaIds: (files || []).map(file => file._id.toString()) })) { return rej('duplicate'); @@ -227,13 +227,13 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res } // 投稿を作成 - const post = await create(user, { + const note = await create(user, { createdAt: new Date(), media: files, poll: poll, text: text, reply, - repost, + renote, cw: cw, tags: tags, app: app, @@ -242,10 +242,10 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res geo }); - const postObj = await pack(post, user); + const noteObj = await pack(note, user); // Reponse res({ - createdPost: postObj + createdNote: noteObj }); }); diff --git a/src/server/api/endpoints/posts/favorites/create.ts b/src/server/api/endpoints/posts/favorites/create.ts deleted file mode 100644 index f537fb7ddf..0000000000 --- a/src/server/api/endpoints/posts/favorites/create.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Favorite from '../../../../../models/favorite'; -import Post from '../../../../../models/post'; - -/** - * Favorite a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Get favoritee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // if already favorited - const exist = await Favorite.findOne({ - postId: post._id, - userId: user._id - }); - - if (exist !== null) { - return rej('already favorited'); - } - - // Create favorite - await Favorite.insert({ - createdAt: new Date(), - postId: post._id, - userId: user._id - }); - - // Send response - res(); -}); diff --git a/src/server/api/endpoints/posts/favorites/delete.ts b/src/server/api/endpoints/posts/favorites/delete.ts deleted file mode 100644 index 28930337a3..0000000000 --- a/src/server/api/endpoints/posts/favorites/delete.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Favorite from '../../../../../models/favorite'; -import Post from '../../../../../models/post'; - -/** - * Unfavorite a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Get favoritee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // if already favorited - const exist = await Favorite.findOne({ - postId: post._id, - userId: user._id - }); - - if (exist === null) { - return rej('already not favorited'); - } - - // Delete favorite - await Favorite.remove({ - _id: exist._id - }); - - // Send response - res(); -}); diff --git a/src/server/api/endpoints/posts/mentions.ts b/src/server/api/endpoints/posts/mentions.ts deleted file mode 100644 index d7302c0620..0000000000 --- a/src/server/api/endpoints/posts/mentions.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../../models/post'; -import getFriends from '../../common/get-friends'; -import { pack } from '../../../../models/post'; - -/** - * Get mentions of myself - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'following' parameter - const [following = false, followingError] = - $(params.following).optional.boolean().$; - if (followingError) return rej('invalid following param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; - if (sinceIdErr) return rej('invalid sinceId param'); - - // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; - if (untilIdErr) return rej('invalid untilId param'); - - // Check if both of sinceId and untilId is specified - if (sinceId && untilId) { - return rej('cannot set sinceId and untilId'); - } - - // Construct query - const query = { - mentions: user._id - } as any; - - const sort = { - _id: -1 - }; - - if (following) { - const followingIds = await getFriends(user._id); - - query.userId = { - $in: followingIds - }; - } - - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (untilId) { - query._id = { - $lt: untilId - }; - } - - // Issue query - const mentions = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(mentions.map(async mention => - await pack(mention, user) - ))); -}); diff --git a/src/server/api/endpoints/posts/polls/recommendation.ts b/src/server/api/endpoints/posts/polls/recommendation.ts deleted file mode 100644 index d706742618..0000000000 --- a/src/server/api/endpoints/posts/polls/recommendation.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Vote from '../../../../../models/poll-vote'; -import Post, { pack } from '../../../../../models/post'; - -/** - * Get recommended polls - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'offset' parameter - const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; - if (offsetErr) return rej('invalid offset param'); - - // Get votes - const votes = await Vote.find({ - userId: user._id - }, { - fields: { - _id: false, - postId: true - } - }); - - const nin = votes && votes.length != 0 ? votes.map(v => v.postId) : []; - - const posts = await Post - .find({ - _id: { - $nin: nin - }, - userId: { - $ne: user._id - }, - poll: { - $exists: true, - $ne: null - } - }, { - limit: limit, - skip: offset, - sort: { - _id: -1 - } - }); - - // Serialize - res(await Promise.all(posts.map(async post => - await pack(post, user, { detail: true })))); -}); diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts deleted file mode 100644 index c270cd09ab..0000000000 --- a/src/server/api/endpoints/posts/polls/vote.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Vote from '../../../../../models/poll-vote'; -import Post from '../../../../../models/post'; -import Watching from '../../../../../models/post-watching'; -import watch from '../../../../../post/watch'; -import { publishPostStream } from '../../../../../publishers/stream'; -import notify from '../../../../../publishers/notify'; - -/** - * Vote poll of a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Get votee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - if (post.poll == null) { - return rej('poll not found'); - } - - // Get 'choice' parameter - const [choice, choiceError] = - $(params.choice).number() - .pipe(c => post.poll.choices.some(x => x.id == c)) - .$; - if (choiceError) return rej('invalid choice param'); - - // if already voted - const exist = await Vote.findOne({ - postId: post._id, - userId: user._id - }); - - if (exist !== null) { - return rej('already voted'); - } - - // Create vote - await Vote.insert({ - createdAt: new Date(), - postId: post._id, - userId: user._id, - choice: choice - }); - - // Send response - res(); - - const inc = {}; - inc[`poll.choices.${findWithAttr(post.poll.choices, 'id', choice)}.votes`] = 1; - - // Increment votes count - await Post.update({ _id: post._id }, { - $inc: inc - }); - - publishPostStream(post._id, 'poll_voted'); - - // Notify - notify(post.userId, user._id, 'poll_vote', { - postId: post._id, - choice: choice - }); - - // Fetch watchers - Watching - .find({ - postId: post._id, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.userId, user._id, 'poll_vote', { - postId: post._id, - choice: choice - }); - }); - }); - - // この投稿をWatchする - if (user.account.settings.autoWatch !== false) { - watch(user._id, post); - } -}); - -function findWithAttr(array, attr, value) { - for (let i = 0; i < array.length; i += 1) { - if (array[i][attr] === value) { - return i; - } - } - return -1; -} diff --git a/src/server/api/endpoints/posts/reactions.ts b/src/server/api/endpoints/posts/reactions.ts deleted file mode 100644 index da733f5337..0000000000 --- a/src/server/api/endpoints/posts/reactions.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../../models/post'; -import Reaction, { pack } from '../../../../models/post-reaction'; - -/** - * Show reactions of a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'offset' parameter - const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; - if (offsetErr) return rej('invalid offset param'); - - // Get 'sort' parameter - const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$; - if (sortError) return rej('invalid sort param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // Issue query - const reactions = await Reaction - .find({ - postId: post._id, - deletedAt: { $exists: false } - }, { - limit: limit, - skip: offset, - sort: { - _id: sort == 'asc' ? 1 : -1 - } - }); - - // Serialize - res(await Promise.all(reactions.map(async reaction => - await pack(reaction, user)))); -}); diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts deleted file mode 100644 index 71fa6a2955..0000000000 --- a/src/server/api/endpoints/posts/reactions/create.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Reaction from '../../../../../models/post-reaction'; -import Post from '../../../../../models/post'; -import create from '../../../../../services/post/reaction/create'; - -/** - * React to a post - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Get 'reaction' parameter - const [reaction, reactionErr] = $(params.reaction).string().or([ - 'like', - 'love', - 'laugh', - 'hmm', - 'surprise', - 'congrats', - 'angry', - 'confused', - 'pudding' - ]).$; - if (reactionErr) return rej('invalid reaction param'); - - // Fetch reactee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - try { - await create(user, post, reaction); - } catch (e) { - rej(e); - } - - res(); -}); diff --git a/src/server/api/endpoints/posts/reactions/delete.ts b/src/server/api/endpoints/posts/reactions/delete.ts deleted file mode 100644 index 3a88bbd7ca..0000000000 --- a/src/server/api/endpoints/posts/reactions/delete.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Reaction from '../../../../../models/post-reaction'; -import Post from '../../../../../models/post'; -// import event from '../../../publishers/stream'; - -/** - * Unreact to a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Fetch unreactee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // if already unreacted - const exist = await Reaction.findOne({ - postId: post._id, - userId: user._id, - deletedAt: { $exists: false } - }); - - if (exist === null) { - return rej('never reacted'); - } - - // Delete reaction - await Reaction.update({ - _id: exist._id - }, { - $set: { - deletedAt: new Date() - } - }); - - // Send response - res(); - - const dec = {}; - dec[`reactionCounts.${exist.reaction}`] = -1; - - // Decrement reactions count - Post.update({ _id: post._id }, { - $inc: dec - }); -}); diff --git a/src/server/api/endpoints/posts/replies.ts b/src/server/api/endpoints/posts/replies.ts deleted file mode 100644 index dd5a95c173..0000000000 --- a/src/server/api/endpoints/posts/replies.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post, { pack } from '../../../../models/post'; - -/** - * Show a replies of a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'offset' parameter - const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; - if (offsetErr) return rej('invalid offset param'); - - // Get 'sort' parameter - const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$; - if (sortError) return rej('invalid sort param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // Issue query - const replies = await Post - .find({ replyId: post._id }, { - limit: limit, - skip: offset, - sort: { - _id: sort == 'asc' ? 1 : -1 - } - }); - - // Serialize - res(await Promise.all(replies.map(async post => - await pack(post, user)))); -}); diff --git a/src/server/api/endpoints/posts/reposts.ts b/src/server/api/endpoints/posts/reposts.ts deleted file mode 100644 index ec6218ca38..0000000000 --- a/src/server/api/endpoints/posts/reposts.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post, { pack } from '../../../../models/post'; - -/** - * Show a reposts of a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; - if (sinceIdErr) return rej('invalid sinceId param'); - - // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; - if (untilIdErr) return rej('invalid untilId param'); - - // Check if both of sinceId and untilId is specified - if (sinceId && untilId) { - return rej('cannot set sinceId and untilId'); - } - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // Construct query - const sort = { - _id: -1 - }; - const query = { - repostId: post._id - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (untilId) { - query._id = { - $lt: untilId - }; - } - - // Issue query - const reposts = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(reposts.map(async post => - await pack(post, user)))); -}); diff --git a/src/server/api/endpoints/posts/search.ts b/src/server/api/endpoints/posts/search.ts deleted file mode 100644 index 21c4e77fdd..0000000000 --- a/src/server/api/endpoints/posts/search.ts +++ /dev/null @@ -1,364 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -const escapeRegexp = require('escape-regexp'); -import Post from '../../../../models/post'; -import User from '../../../../models/user'; -import Mute from '../../../../models/mute'; -import getFriends from '../../common/get-friends'; -import { pack } from '../../../../models/post'; - -/** - * Search a post - * - * @param {any} params - * @param {any} me - * @return {Promise} - */ -module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'text' parameter - const [text, textError] = $(params.text).optional.string().$; - if (textError) return rej('invalid text param'); - - // Get 'includeUserIds' parameter - const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array('id').$; - if (includeUserIdsErr) return rej('invalid includeUserIds param'); - - // Get 'excludeUserIds' parameter - const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array('id').$; - if (excludeUserIdsErr) return rej('invalid excludeUserIds param'); - - // Get 'includeUserUsernames' parameter - const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array('string').$; - if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param'); - - // Get 'excludeUserUsernames' parameter - const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array('string').$; - if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param'); - - // Get 'following' parameter - const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$; - if (followingErr) return rej('invalid following param'); - - // Get 'mute' parameter - const [mute = 'mute_all', muteErr] = $(params.mute).optional.string().$; - if (muteErr) return rej('invalid mute param'); - - // Get 'reply' parameter - const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$; - if (replyErr) return rej('invalid reply param'); - - // Get 'repost' parameter - const [repost = null, repostErr] = $(params.repost).optional.nullable.boolean().$; - if (repostErr) return rej('invalid repost param'); - - // Get 'media' parameter - const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$; - if (mediaErr) return rej('invalid media param'); - - // Get 'poll' parameter - const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$; - if (pollErr) return rej('invalid poll param'); - - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; - if (sinceDateErr) throw 'invalid sinceDate param'; - - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; - if (untilDateErr) throw 'invalid untilDate param'; - - // Get 'offset' parameter - const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; - if (offsetErr) return rej('invalid offset param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$; - if (limitErr) return rej('invalid limit param'); - - let includeUsers = includeUserIds; - if (includeUserUsernames != null) { - const ids = (await Promise.all(includeUserUsernames.map(async (username) => { - const _user = await User.findOne({ - usernameLower: username.toLowerCase() - }); - return _user ? _user._id : null; - }))).filter(id => id != null); - includeUsers = includeUsers.concat(ids); - } - - let excludeUsers = excludeUserIds; - if (excludeUserUsernames != null) { - const ids = (await Promise.all(excludeUserUsernames.map(async (username) => { - const _user = await User.findOne({ - usernameLower: username.toLowerCase() - }); - return _user ? _user._id : null; - }))).filter(id => id != null); - excludeUsers = excludeUsers.concat(ids); - } - - search(res, rej, me, text, includeUsers, excludeUsers, following, - mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit); -}); - -async function search( - res, rej, me, text, includeUserIds, excludeUserIds, following, - mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) { - - let q: any = { - $and: [] - }; - - const push = x => q.$and.push(x); - - if (text) { - // 完全一致検索 - if (/"""(.+?)"""/.test(text)) { - const x = text.match(/"""(.+?)"""/)[1]; - push({ - text: x - }); - } else { - const tags = text.split(' ').filter(x => x[0] == '#'); - if (tags) { - push({ - $and: tags.map(x => ({ - tags: x - })) - }); - } - - push({ - $and: text.split(' ').map(x => ({ - // キーワードが-で始まる場合そのキーワードを除外する - text: x[0] == '-' ? { - $not: new RegExp(escapeRegexp(x.substr(1))) - } : new RegExp(escapeRegexp(x)) - })) - }); - } - } - - if (includeUserIds && includeUserIds.length != 0) { - push({ - userId: { - $in: includeUserIds - } - }); - } else if (excludeUserIds && excludeUserIds.length != 0) { - push({ - userId: { - $nin: excludeUserIds - } - }); - } - - if (following != null && me != null) { - const ids = await getFriends(me._id, false); - push({ - userId: following ? { - $in: ids - } : { - $nin: ids.concat(me._id) - } - }); - } - - if (me != null) { - const mutes = await Mute.find({ - muterId: me._id, - deletedAt: { $exists: false } - }); - const mutedUserIds = mutes.map(m => m.muteeId); - - switch (mute) { - case 'mute_all': - push({ - userId: { - $nin: mutedUserIds - }, - '_reply.userId': { - $nin: mutedUserIds - }, - '_repost.userId': { - $nin: mutedUserIds - } - }); - break; - case 'mute_related': - push({ - '_reply.userId': { - $nin: mutedUserIds - }, - '_repost.userId': { - $nin: mutedUserIds - } - }); - break; - case 'mute_direct': - push({ - userId: { - $nin: mutedUserIds - } - }); - break; - case 'direct_only': - push({ - userId: { - $in: mutedUserIds - } - }); - break; - case 'related_only': - push({ - $or: [{ - '_reply.userId': { - $in: mutedUserIds - } - }, { - '_repost.userId': { - $in: mutedUserIds - } - }] - }); - break; - case 'all_only': - push({ - $or: [{ - userId: { - $in: mutedUserIds - } - }, { - '_reply.userId': { - $in: mutedUserIds - } - }, { - '_repost.userId': { - $in: mutedUserIds - } - }] - }); - break; - } - } - - if (reply != null) { - if (reply) { - push({ - replyId: { - $exists: true, - $ne: null - } - }); - } else { - push({ - $or: [{ - replyId: { - $exists: false - } - }, { - replyId: null - }] - }); - } - } - - if (repost != null) { - if (repost) { - push({ - repostId: { - $exists: true, - $ne: null - } - }); - } else { - push({ - $or: [{ - repostId: { - $exists: false - } - }, { - repostId: null - }] - }); - } - } - - if (media != null) { - if (media) { - push({ - mediaIds: { - $exists: true, - $ne: null - } - }); - } else { - push({ - $or: [{ - mediaIds: { - $exists: false - } - }, { - mediaIds: null - }] - }); - } - } - - if (poll != null) { - if (poll) { - push({ - poll: { - $exists: true, - $ne: null - } - }); - } else { - push({ - $or: [{ - poll: { - $exists: false - } - }, { - poll: null - }] - }); - } - } - - if (sinceDate) { - push({ - createdAt: { - $gt: new Date(sinceDate) - } - }); - } - - if (untilDate) { - push({ - createdAt: { - $lt: new Date(untilDate) - } - }); - } - - if (q.$and.length == 0) { - q = {}; - } - - // Search posts - const posts = await Post - .find(q, { - sort: { - _id: -1 - }, - limit: max, - skip: offset - }); - - // Serialize - res(await Promise.all(posts.map(async post => - await pack(post, me)))); -} diff --git a/src/server/api/endpoints/posts/show.ts b/src/server/api/endpoints/posts/show.ts deleted file mode 100644 index e1781b545c..0000000000 --- a/src/server/api/endpoints/posts/show.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post, { pack } from '../../../../models/post'; - -/** - * Show a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Get post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // Serialize - res(await pack(post, user, { - detail: true - })); -}); diff --git a/src/server/api/endpoints/posts/timeline.ts b/src/server/api/endpoints/posts/timeline.ts deleted file mode 100644 index b58d25fa80..0000000000 --- a/src/server/api/endpoints/posts/timeline.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import rap from '@prezzemolo/rap'; -import Post from '../../../../models/post'; -import Mute from '../../../../models/mute'; -import ChannelWatching from '../../../../models/channel-watching'; -import getFriends from '../../common/get-friends'; -import { pack } from '../../../../models/post'; - -/** - * Get timeline of myself - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise} - */ -module.exports = async (params, user, app) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) throw 'invalid limit param'; - - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; - if (sinceIdErr) throw 'invalid sinceId param'; - - // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; - if (untilIdErr) throw 'invalid untilId param'; - - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; - if (sinceDateErr) throw 'invalid sinceDate param'; - - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; - if (untilDateErr) throw 'invalid untilDate param'; - - // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { - throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; - } - - const { followingIds, watchingChannelIds, mutedUserIds } = await rap({ - // ID list of the user itself and other users who the user follows - followingIds: getFriends(user._id), - - // Watchしているチャンネルを取得 - watchingChannelIds: ChannelWatching.find({ - userId: user._id, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }).then(watches => watches.map(w => w.channelId)), - - // ミュートしているユーザーを取得 - mutedUserIds: Mute.find({ - muterId: user._id, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }).then(ms => ms.map(m => m.muteeId)) - }); - - //#region Construct query - const sort = { - _id: -1 - }; - - const query = { - $or: [{ - // フォローしている人のタイムラインへの投稿 - userId: { - $in: followingIds - }, - // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る - $or: [{ - channelId: { - $exists: false - } - }, { - channelId: null - }] - }, { - // Watchしているチャンネルへの投稿 - channelId: { - $in: watchingChannelIds - } - }], - // mute - userId: { - $nin: mutedUserIds - }, - '_reply.userId': { - $nin: mutedUserIds - }, - '_repost.userId': { - $nin: mutedUserIds - }, - } as any; - - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (untilId) { - query._id = { - $lt: untilId - }; - } else if (sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(sinceDate) - }; - } else if (untilDate) { - query.createdAt = { - $lt: new Date(untilDate) - }; - } - //#endregion - - // Issue query - const timeline = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - return await Promise.all(timeline.map(post => pack(post, user))); -}; diff --git a/src/server/api/endpoints/posts/trend.ts b/src/server/api/endpoints/posts/trend.ts deleted file mode 100644 index dbee169138..0000000000 --- a/src/server/api/endpoints/posts/trend.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Module dependencies - */ -const ms = require('ms'); -import $ from 'cafy'; -import Post, { pack } from '../../../../models/post'; - -/** - * Get trend posts - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'offset' parameter - const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; - if (offsetErr) return rej('invalid offset param'); - - // Get 'reply' parameter - const [reply, replyErr] = $(params.reply).optional.boolean().$; - if (replyErr) return rej('invalid reply param'); - - // Get 'repost' parameter - const [repost, repostErr] = $(params.repost).optional.boolean().$; - if (repostErr) return rej('invalid repost param'); - - // Get 'media' parameter - const [media, mediaErr] = $(params.media).optional.boolean().$; - if (mediaErr) return rej('invalid media param'); - - // Get 'poll' parameter - const [poll, pollErr] = $(params.poll).optional.boolean().$; - if (pollErr) return rej('invalid poll param'); - - const query = { - createdAt: { - $gte: new Date(Date.now() - ms('1days')) - }, - repostCount: { - $gt: 0 - } - } as any; - - if (reply != undefined) { - query.replyId = reply ? { $exists: true, $ne: null } : null; - } - - if (repost != undefined) { - query.repostId = repost ? { $exists: true, $ne: null } : null; - } - - if (media != undefined) { - query.mediaIds = media ? { $exists: true, $ne: null } : null; - } - - if (poll != undefined) { - query.poll = poll ? { $exists: true, $ne: null } : null; - } - - // Issue query - const posts = await Post - .find(query, { - limit: limit, - skip: offset, - sort: { - repostCount: -1, - _id: -1 - } - }); - - // Serialize - res(await Promise.all(posts.map(async post => - await pack(post, user, { detail: true })))); -}); diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts index 0fb0c44b0f..52e5195484 100644 --- a/src/server/api/endpoints/stats.ts +++ b/src/server/api/endpoints/stats.ts @@ -1,13 +1,13 @@ /** * Module dependencies */ -import Post from '../../../models/post'; +import Note from '../../../models/note'; import User from '../../../models/user'; /** * @swagger * /stats: - * post: + * note: * summary: Show the misskey's statistics * responses: * 200: @@ -15,8 +15,8 @@ import User from '../../../models/user'; * schema: * type: object * properties: - * postsCount: - * description: count of all posts of misskey + * notesCount: + * description: count of all notes of misskey * type: number * usersCount: * description: count of all users of misskey @@ -35,14 +35,14 @@ import User from '../../../models/user'; * @return {Promise} */ module.exports = params => new Promise(async (res, rej) => { - const postsCount = await Post + const notesCount = await Note .count(); const usersCount = await User .count(); res({ - postsCount: postsCount, + notesCount: notesCount, usersCount: usersCount }); }); diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts index 3a116c8e26..7a98f44e98 100644 --- a/src/server/api/endpoints/users/get_frequently_replied_users.ts +++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../../../models/post'; +import Note from '../../../../models/note'; import User, { pack } from '../../../../models/user'; module.exports = (params, me) => new Promise(async (res, rej) => { @@ -27,8 +27,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => { return rej('user not found'); } - // Fetch recent posts - const recentPosts = await Post.find({ + // Fetch recent notes + const recentNotes = await Note.find({ userId: user._id, replyId: { $exists: true, @@ -46,13 +46,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => { }); // 投稿が少なかったら中断 - if (recentPosts.length === 0) { + if (recentNotes.length === 0) { return res([]); } - const replyTargetPosts = await Post.find({ + const replyTargetNotes = await Note.find({ _id: { - $in: recentPosts.map(p => p.replyId) + $in: recentNotes.map(p => p.replyId) }, userId: { $ne: user._id @@ -66,9 +66,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const repliedUsers = {}; - // Extract replies from recent posts - replyTargetPosts.forEach(post => { - const userId = post.userId.toString(); + // Extract replies from recent notes + replyTargetNotes.forEach(note => { + const userId = note.userId.toString(); if (repliedUsers[userId]) { repliedUsers[userId]++; } else { diff --git a/src/server/api/endpoints/users/posts.ts b/src/server/api/endpoints/users/posts.ts index b6c533fb5b..f9f6345e34 100644 --- a/src/server/api/endpoints/users/posts.ts +++ b/src/server/api/endpoints/users/posts.ts @@ -3,11 +3,11 @@ */ import $ from 'cafy'; import getHostLower from '../../common/get-host-lower'; -import Post, { pack } from '../../../../models/post'; +import Note, { pack } from '../../../../models/note'; import User from '../../../../models/user'; /** - * Get posts of a user + * Get notes of a user * * @param {any} params * @param {any} me @@ -124,14 +124,14 @@ module.exports = (params, me) => new Promise(async (res, rej) => { //#endregion // Issue query - const posts = await Post + const notes = await Note .find(query, { limit: limit, sort: sort }); // Serialize - res(await Promise.all(posts.map(async (post) => - await pack(post, me) + res(await Promise.all(notes.map(async (note) => + await pack(note, me) ))); }); -- cgit v1.2.3-freya