From b3f42e62af698a67c2250533c437569559f1fdf9 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 29 Dec 2016 07:49:51 +0900 Subject: Initial commit :four_leaf_clover: --- src/api/endpoints/posts/context.js | 83 +++++++ src/api/endpoints/posts/create.js | 345 ++++++++++++++++++++++++++++ src/api/endpoints/posts/favorites/create.js | 56 +++++ src/api/endpoints/posts/favorites/delete.js | 52 +++++ src/api/endpoints/posts/likes.js | 77 +++++++ src/api/endpoints/posts/likes/create.js | 93 ++++++++ src/api/endpoints/posts/likes/delete.js | 80 +++++++ src/api/endpoints/posts/mentions.js | 85 +++++++ src/api/endpoints/posts/replies.js | 73 ++++++ src/api/endpoints/posts/reposts.js | 85 +++++++ src/api/endpoints/posts/search.js | 138 +++++++++++ src/api/endpoints/posts/show.js | 40 ++++ src/api/endpoints/posts/timeline.js | 78 +++++++ 13 files changed, 1285 insertions(+) create mode 100644 src/api/endpoints/posts/context.js create mode 100644 src/api/endpoints/posts/create.js create mode 100644 src/api/endpoints/posts/favorites/create.js create mode 100644 src/api/endpoints/posts/favorites/delete.js create mode 100644 src/api/endpoints/posts/likes.js create mode 100644 src/api/endpoints/posts/likes/create.js create mode 100644 src/api/endpoints/posts/likes/delete.js create mode 100644 src/api/endpoints/posts/mentions.js create mode 100644 src/api/endpoints/posts/replies.js create mode 100644 src/api/endpoints/posts/reposts.js create mode 100644 src/api/endpoints/posts/search.js create mode 100644 src/api/endpoints/posts/show.js create mode 100644 src/api/endpoints/posts/timeline.js (limited to 'src/api/endpoints/posts') diff --git a/src/api/endpoints/posts/context.js b/src/api/endpoints/posts/context.js new file mode 100644 index 0000000000..5f040b8505 --- /dev/null +++ b/src/api/endpoints/posts/context.js @@ -0,0 +1,83 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Post from '../../models/post'; +import serialize from '../../serializers/post'; + +/** + * Show a context of a post + * + * @param {Object} params + * @param {Object} user + * @return {Promise} + */ +module.exports = (params, user) => + new Promise(async (res, rej) => +{ + // Get 'post_id' parameter + const postId = params.post_id; + if (postId === undefined || postId === null) { + return rej('post_id is required'); + } + + // Get 'limit' parameter + let limit = params.limit; + if (limit !== undefined && limit !== null) { + limit = parseInt(limit, 10); + + // From 1 to 100 + if (!(1 <= limit && limit <= 100)) { + return rej('invalid limit range'); + } + } else { + limit = 10; + } + + // Get 'offset' parameter + let offset = params.offset; + if (offset !== undefined && offset !== null) { + offset = parseInt(offset, 10); + } else { + offset = 0; + } + + // Lookup post + const post = await Post.findOne({ + _id: new mongo.ObjectID(postId) + }); + + if (post === null) { + return rej('post not found', '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.reply_to_id) { + await get(p.reply_to_id); + } + } + + if (post.reply_to_id) { + await get(post.reply_to_id); + } + + // Serialize + res(await Promise.all(context.map(async post => + await serialize(post, user)))); +}); diff --git a/src/api/endpoints/posts/create.js b/src/api/endpoints/posts/create.js new file mode 100644 index 0000000000..cdcbf4f966 --- /dev/null +++ b/src/api/endpoints/posts/create.js @@ -0,0 +1,345 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import parse from '../../../common/text'; +import Post from '../../models/post'; +import User from '../../models/user'; +import Following from '../../models/following'; +import DriveFile from '../../models/drive-file'; +import serialize from '../../serializers/post'; +import createFile from '../../common/add-file-to-drive'; +import notify from '../../common/notify'; +import event from '../../event'; + +/** + * 最大文字数 + */ +const maxTextLength = 300; + +/** + * 添付できるファイルの数 + */ +const maxMediaCount = 4; + +/** + * Create a post + * + * @param {Object} params + * @param {Object} user + * @param {Object} app + * @return {Promise} + */ +module.exports = (params, user, app) => + new Promise(async (res, rej) => +{ + // Get 'text' parameter + let text = params.text; + if (text !== undefined && text !== null) { + text = text.trim(); + if (text.length == 0) { + text = null; + } else if (text.length > maxTextLength) { + return rej('too long text'); + } + } else { + text = null; + } + + // Get 'media_ids' parameter + let media = params.media_ids; + let files = []; + if (media !== undefined && media !== null) { + media = media.split(','); + + if (media.length > maxMediaCount) { + return rej('too many media'); + } + + // Drop duplicates + media = media.filter((x, i, s) => s.indexOf(x) == i); + + // Fetch files + // forEach だと途中でエラーなどがあっても return できないので + // 敢えて for を使っています。 + for (let i = 0; i < media.length; i++) { + const image = media[i]; + + // Fetch file + // SELECT _id + const entity = await DriveFile.findOne({ + _id: new mongo.ObjectID(image), + user_id: user._id + }, { + _id: true + }); + + if (entity === null) { + return rej('file not found'); + } else { + files.push(entity); + } + } + } else { + files = null; + } + + // Get 'repost_id' parameter + let repost = params.repost_id; + if (repost !== undefined && repost !== null) { + // Fetch repost to post + repost = await Post.findOne({ + _id: new mongo.ObjectID(repost) + }); + + if (repost == null) { + return rej('repostee is not found'); + } else if (repost.repost_id && !repost.text && !repost.media_ids) { + return rej('cannot repost to repost'); + } + + // Fetch recently post + const latestPost = await Post.findOne({ + user_id: user._id + }, {}, { + sort: { + _id: -1 + } + }); + + // 直近と同じRepost対象かつ引用じゃなかったらエラー + if (latestPost && + latestPost.repost_id && + latestPost.repost_id.equals(repost._id) && + text === null && files === null) { + return rej('二重Repostです(NEED TRANSLATE)'); + } + + // 直近がRepost対象かつ引用じゃなかったらエラー + if (latestPost && + latestPost._id.equals(repost._id) && + text === null && files === null) { + return rej('二重Repostです(NEED TRANSLATE)'); + } + } else { + repost = null; + } + + // Get 'reply_to_id' parameter + let replyTo = params.reply_to_id; + if (replyTo !== undefined && replyTo !== null) { + replyTo = await Post.findOne({ + _id: new mongo.ObjectID(replyTo) + }); + + if (replyTo === null) { + return rej('reply to post is not found'); + } + + // 返信対象が引用でないRepostだったらエラー + if (replyTo.repost_id && !replyTo.text && !replyTo.media_ids) { + return rej('cannot reply to repost'); + } + } else { + replyTo = null; + } + + // テキストが無いかつ添付ファイルが無いかつRepostも無かったらエラー + if (text === null && files === null && repost === null) { + return rej('text, media_ids or repost_id is required'); + } + + // 投稿を作成 + const inserted = await Post.insert({ + created_at: new Date(), + media_ids: media ? files.map(file => file._id) : undefined, + reply_to_id: replyTo ? replyTo._id : undefined, + repost_id: repost ? repost._id : undefined, + text: text, + user_id: user._id, + app_id: app ? app._id : null + }); + + const post = inserted.ops[0]; + + // Serialize + const postObj = await serialize(post); + + // Reponse + res(postObj); + + //-------------------------------- + // Post processes + + let mentions = []; + + function addMention(mentionee, type) { + // Reject if already added + if (mentions.some(x => x.equals(mentionee))) return; + + // Add mention + mentions.push(mentionee); + + // Publish event + if (!user._id.equals(mentionee)) { + event(mentionee, type, postObj); + } + } + + // Publish event to myself's stream + event(user._id, 'post', postObj); + + // Fetch all followers + const followers = await Following + .find({ + followee_id: user._id, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }, { + follower_id: true, + _id: false + }) + .toArray(); + + // Publish event to followers stream + followers.forEach(following => + event(following.follower_id, 'post', postObj)); + + // Increment my posts count + User.updateOne({ _id: user._id }, { + $inc: { + posts_count: 1 + } + }); + + // If has in reply to post + if (replyTo) { + // Increment replies count + Post.updateOne({ _id: replyTo._id }, { + $inc: { + replies_count: 1 + } + }); + + // 自分自身へのリプライでない限りは通知を作成 + notify(replyTo.user_id, user._id, 'reply', { + post_id: post._id + }); + + // Add mention + addMention(replyTo.user_id, 'reply'); + } + + // If it is repost + if (repost) { + // Notify + const type = text ? 'quote' : 'repost'; + notify(repost.user_id, user._id, type, { + post_id: post._id + }); + + // If it is quote repost + if (text) { + // Add mention + addMention(repost.user_id, 'quote'); + } else { + // Publish event + if (!user._id.equals(repost.user_id)) { + event(repost.user_id, 'repost', postObj); + } + } + + // 今までで同じ投稿をRepostしているか + const existRepost = await Post.findOne({ + user_id: user._id, + repost_id: repost._id, + _id: { + $ne: post._id + } + }); + + if (!existRepost) { + // Update repostee status + Post.updateOne({ _id: repost._id }, { + $inc: { + repost_count: 1 + } + }); + } + } + + // If has text content + if (text) { + // Analyze + const tokens = parse(text); + + // Extract a hashtags + const hashtags = tokens + .filter(t => t.type == 'hashtag') + .map(t => t.hashtag) + // Drop dupulicates + .filter((v, i, s) => s.indexOf(v) == i); + + // ハッシュタグをデータベースに登録 + //registerHashtags(user, hashtags); + + // Extract an '@' mentions + const atMentions = tokens + .filter(t => t.type == 'mention') + .map(m => m.username) + // Drop dupulicates + .filter((v, i, s) => s.indexOf(v) == i); + + // Resolve all mentions + await Promise.all(atMentions.map(async (mention) => { + // Fetch mentioned user + // SELECT _id + const mentionee = await User + .findOne({ + username_lower: mention.toLowerCase() + }, { _id: true }); + + // When mentioned user not found + if (mentionee == null) return; + + // 既に言及されたユーザーに対する返信や引用repostの場合も無視 + if (replyTo && replyTo.user_id.equals(mentionee._id)) return; + if (repost && repost.user_id.equals(mentionee._id)) return; + + // Add mention + addMention(mentionee._id, 'mention'); + + // Create notification + notify(mentionee._id, user._id, 'mention', { + post_id: post._id + }); + + return; + })); + } + + // Register to search database + if (text && config.elasticsearch.enable) { + const es = require('../../../db/elasticsearch'); + + es.index({ + index: 'misskey', + type: 'post', + id: post._id.toString(), + body: { + text: post.text + } + }); + } + + // Append mentions data + if (mentions.length > 0) { + Post.updateOne({ _id: post._id }, { + $set: { + mentions: mentions + } + }); + } +}); diff --git a/src/api/endpoints/posts/favorites/create.js b/src/api/endpoints/posts/favorites/create.js new file mode 100644 index 0000000000..d20a523d5d --- /dev/null +++ b/src/api/endpoints/posts/favorites/create.js @@ -0,0 +1,56 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Favorite from '../../models/favorite'; +import Post from '../../models/post'; + +/** + * Favorite a post + * + * @param {Object} params + * @param {Object} user + * @return {Promise} + */ +module.exports = (params, user) => + new Promise(async (res, rej) => +{ + // Get 'post_id' parameter + let postId = params.post_id; + if (postId === undefined || postId === null) { + return rej('post_id is required'); + } + + // Get favoritee + const post = await Post.findOne({ + _id: new mongo.ObjectID(postId) + }); + + if (post === null) { + return rej('post not found'); + } + + // Check arleady favorited + const exist = await Favorite.findOne({ + post_id: post._id, + user_id: user._id + }); + + if (exist !== null) { + return rej('already favorited'); + } + + // Create favorite + const inserted = await Favorite.insert({ + created_at: new Date(), + post_id: post._id, + user_id: user._id + }); + + const favorite = inserted.ops[0]; + + // Send response + res(); +}); diff --git a/src/api/endpoints/posts/favorites/delete.js b/src/api/endpoints/posts/favorites/delete.js new file mode 100644 index 0000000000..e250d1772c --- /dev/null +++ b/src/api/endpoints/posts/favorites/delete.js @@ -0,0 +1,52 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Favorite from '../../models/favorite'; +import Post from '../../models/post'; + +/** + * Unfavorite a post + * + * @param {Object} params + * @param {Object} user + * @return {Promise} + */ +module.exports = (params, user) => + new Promise(async (res, rej) => +{ + // Get 'post_id' parameter + let postId = params.post_id; + if (postId === undefined || postId === null) { + return rej('post_id is required'); + } + + // Get favoritee + const post = await Post.findOne({ + _id: new mongo.ObjectID(postId) + }); + + if (post === null) { + return rej('post not found'); + } + + // Check arleady favorited + const exist = await Favorite.findOne({ + post_id: post._id, + user_id: user._id + }); + + if (exist === null) { + return rej('already not favorited'); + } + + // Delete favorite + await Favorite.deleteOne({ + _id: exist._id + }); + + // Send response + res(); +}); diff --git a/src/api/endpoints/posts/likes.js b/src/api/endpoints/posts/likes.js new file mode 100644 index 0000000000..4778189fc6 --- /dev/null +++ b/src/api/endpoints/posts/likes.js @@ -0,0 +1,77 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Post from '../../models/post'; +import Like from '../../models/like'; +import serialize from '../../serializers/user'; + +/** + * Show a likes of a post + * + * @param {Object} params + * @param {Object} user + * @return {Promise} + */ +module.exports = (params, user) => + new Promise(async (res, rej) => +{ + // Get 'post_id' parameter + const postId = params.post_id; + if (postId === undefined || postId === null) { + return rej('post_id is required'); + } + + // Get 'limit' parameter + let limit = params.limit; + if (limit !== undefined && limit !== null) { + limit = parseInt(limit, 10); + + // From 1 to 100 + if (!(1 <= limit && limit <= 100)) { + return rej('invalid limit range'); + } + } else { + limit = 10; + } + + // Get 'offset' parameter + let offset = params.offset; + if (offset !== undefined && offset !== null) { + offset = parseInt(offset, 10); + } else { + offset = 0; + } + + // Get 'sort' parameter + let sort = params.sort || 'desc'; + + // Lookup post + const post = await Post.findOne({ + _id: new mongo.ObjectID(postId) + }); + + if (post === null) { + return rej('post not found'); + } + + // Issue query + const likes = await Like + .find({ + post_id: post._id, + deleted_at: { $exists: false } + }, {}, { + limit: limit, + skip: offset, + sort: { + _id: sort == 'asc' ? 1 : -1 + } + }) + .toArray(); + + // Serialize + res(await Promise.all(likes.map(async like => + await serialize(like.user_id, user)))); +}); diff --git a/src/api/endpoints/posts/likes/create.js b/src/api/endpoints/posts/likes/create.js new file mode 100644 index 0000000000..eb35c1e4b0 --- /dev/null +++ b/src/api/endpoints/posts/likes/create.js @@ -0,0 +1,93 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Like from '../../../models/like'; +import Post from '../../../models/post'; +import User from '../../../models/user'; +import notify from '../../../common/notify'; +import event from '../../../event'; +import serializeUser from '../../../serializers/user'; +import serializePost from '../../../serializers/post'; + +/** + * Like a post + * + * @param {Object} params + * @param {Object} user + * @return {Promise} + */ +module.exports = (params, user) => + new Promise(async (res, rej) => +{ + // Get 'post_id' parameter + let postId = params.post_id; + if (postId === undefined || postId === null) { + return rej('post_id is required'); + } + + // Get likee + const post = await Post.findOne({ + _id: new mongo.ObjectID(postId) + }); + + if (post === null) { + return rej('post not found'); + } + + // Myself + if (post.user_id.equals(user._id)) { + return rej('-need-translate-'); + } + + // Check arleady liked + const exist = await Like.findOne({ + post_id: post._id, + user_id: user._id, + deleted_at: { $exists: false } + }); + + if (exist !== null) { + return rej('already liked'); + } + + // Create like + const inserted = await Like.insert({ + created_at: new Date(), + post_id: post._id, + user_id: user._id + }); + + const like = inserted.ops[0]; + + // Send response + res(); + + // Increment likes count + Post.updateOne({ _id: post._id }, { + $inc: { + likes_count: 1 + } + }); + + // Increment user likes count + User.updateOne({ _id: user._id }, { + $inc: { + likes_count: 1 + } + }); + + // Increment user liked count + User.updateOne({ _id: post.user_id }, { + $inc: { + liked_count: 1 + } + }); + + // Notify + notify(post.user_id, user._id, 'like', { + post_id: post._id + }); +}); diff --git a/src/api/endpoints/posts/likes/delete.js b/src/api/endpoints/posts/likes/delete.js new file mode 100644 index 0000000000..b60df63af5 --- /dev/null +++ b/src/api/endpoints/posts/likes/delete.js @@ -0,0 +1,80 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Like from '../../../models/like'; +import Post from '../../../models/post'; +import User from '../../../models/user'; +// import event from '../../../event'; + +/** + * Unlike a post + * + * @param {Object} params + * @param {Object} user + * @return {Promise} + */ +module.exports = (params, user) => + new Promise(async (res, rej) => +{ + // Get 'post_id' parameter + let postId = params.post_id; + if (postId === undefined || postId === null) { + return rej('post_id is required'); + } + + // Get likee + const post = await Post.findOne({ + _id: new mongo.ObjectID(postId) + }); + + if (post === null) { + return rej('post not found'); + } + + // Check arleady liked + const exist = await Like.findOne({ + post_id: post._id, + user_id: user._id, + deleted_at: { $exists: false } + }); + + if (exist === null) { + return rej('already not liked'); + } + + // Delete like + await Like.updateOne({ + _id: exist._id + }, { + $set: { + deleted_at: new Date() + } + }); + + // Send response + res(); + + // Decrement likes count + Post.updateOne({ _id: post._id }, { + $inc: { + likes_count: -1 + } + }); + + // Decrement user likes count + User.updateOne({ _id: user._id }, { + $inc: { + likes_count: -1 + } + }); + + // Decrement user liked count + User.updateOne({ _id: post.user_id }, { + $inc: { + liked_count: -1 + } + }); +}); diff --git a/src/api/endpoints/posts/mentions.js b/src/api/endpoints/posts/mentions.js new file mode 100644 index 0000000000..6358e1f4a9 --- /dev/null +++ b/src/api/endpoints/posts/mentions.js @@ -0,0 +1,85 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Post from '../../models/post'; +import getFriends from '../../common/get-friends'; +import serialize from '../../serializers/post'; + +/** + * Get mentions of myself + * + * @param {Object} params + * @param {Object} user + * @return {Promise} + */ +module.exports = (params, user) => + new Promise(async (res, rej) => +{ + // Get 'following' parameter + const following = params.following === 'true'; + + // Get 'limit' parameter + let limit = params.limit; + if (limit !== undefined && limit !== null) { + limit = parseInt(limit, 10); + + // From 1 to 100 + if (!(1 <= limit && limit <= 100)) { + return rej('invalid limit range'); + } + } else { + limit = 10; + } + + const since = params.since_id || null; + const max = params.max_id || null; + + // Check if both of since_id and max_id is specified + if (since !== null && max !== null) { + return rej('cannot set since_id and max_id'); + } + + // Construct query + const query = { + mentions: user._id + }; + + const sort = { + _id: -1 + }; + + if (following) { + const followingIds = await getFriends(user._id); + + query.user_id = { + $in: followingIds + }; + } + + if (since) { + sort._id = 1; + query._id = { + $gt: new mongo.ObjectID(since) + }; + } else if (max) { + query._id = { + $lt: new mongo.ObjectID(max) + }; + } + + // Issue query + const mentions = await Post + .find(query, {}, { + limit: limit, + sort: sort + }) + .toArray(); + + // Serialize + res(await Promise.all(mentions.map(async mention => + await serialize(mention, user) + ))); +}); diff --git a/src/api/endpoints/posts/replies.js b/src/api/endpoints/posts/replies.js new file mode 100644 index 0000000000..5eab6f896f --- /dev/null +++ b/src/api/endpoints/posts/replies.js @@ -0,0 +1,73 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Post from '../../models/post'; +import serialize from '../../serializers/post'; + +/** + * Show a replies of a post + * + * @param {Object} params + * @param {Object} user + * @return {Promise} + */ +module.exports = (params, user) => + new Promise(async (res, rej) => +{ + // Get 'post_id' parameter + const postId = params.post_id; + if (postId === undefined || postId === null) { + return rej('post_id is required'); + } + + // Get 'limit' parameter + let limit = params.limit; + if (limit !== undefined && limit !== null) { + limit = parseInt(limit, 10); + + // From 1 to 100 + if (!(1 <= limit && limit <= 100)) { + return rej('invalid limit range'); + } + } else { + limit = 10; + } + + // Get 'offset' parameter + let offset = params.offset; + if (offset !== undefined && offset !== null) { + offset = parseInt(offset, 10); + } else { + offset = 0; + } + + // Get 'sort' parameter + let sort = params.sort || 'desc'; + + // Lookup post + const post = await Post.findOne({ + _id: new mongo.ObjectID(postId) + }); + + if (post === null) { + return rej('post not found', 'POST_NOT_FOUND'); + } + + // Issue query + const replies = await Post + .find({ reply_to_id: post._id }, {}, { + limit: limit, + skip: offset, + sort: { + _id: sort == 'asc' ? 1 : -1 + } + }) + .toArray(); + + // Serialize + res(await Promise.all(replies.map(async post => + await serialize(post, user)))); +}); diff --git a/src/api/endpoints/posts/reposts.js b/src/api/endpoints/posts/reposts.js new file mode 100644 index 0000000000..8b418a682f --- /dev/null +++ b/src/api/endpoints/posts/reposts.js @@ -0,0 +1,85 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Post from '../../models/post'; +import serialize from '../../serializers/post'; + +/** + * Show a reposts of a post + * + * @param {Object} params + * @param {Object} user + * @return {Promise} + */ +module.exports = (params, user) => + new Promise(async (res, rej) => +{ + // Get 'post_id' parameter + const postId = params.post_id; + if (postId === undefined || postId === null) { + return rej('post_id is required'); + } + + // Get 'limit' parameter + let limit = params.limit; + if (limit !== undefined && limit !== null) { + limit = parseInt(limit, 10); + + // From 1 to 100 + if (!(1 <= limit && limit <= 100)) { + return rej('invalid limit range'); + } + } else { + limit = 10; + } + + const since = params.since_id || null; + const max = params.max_id || null; + + // Check if both of since_id and max_id is specified + if (since !== null && max !== null) { + return rej('cannot set since_id and max_id'); + } + + // Lookup post + const post = await Post.findOne({ + _id: new mongo.ObjectID(postId) + }); + + if (post === null) { + return rej('post not found', 'POST_NOT_FOUND'); + } + + // Construct query + const sort = { + created_at: -1 + }; + const query = { + repost_id: post._id + }; + if (since !== null) { + sort.created_at = 1; + query._id = { + $gt: new mongo.ObjectID(since) + }; + } else if (max !== null) { + query._id = { + $lt: new mongo.ObjectID(max) + }; + } + + // Issue query + const reposts = await Post + .find(query, {}, { + limit: limit, + sort: sort + }) + .toArray(); + + // Serialize + res(await Promise.all(reposts.map(async post => + await serialize(post, user)))); +}); diff --git a/src/api/endpoints/posts/search.js b/src/api/endpoints/posts/search.js new file mode 100644 index 0000000000..0f214ef7ae --- /dev/null +++ b/src/api/endpoints/posts/search.js @@ -0,0 +1,138 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Post from '../../models/post'; +import serialize from '../../serializers/post'; +const escapeRegexp = require('escape-regexp'); + +/** + * Search a post + * + * @param {Object} params + * @param {Object} me + * @return {Promise} + */ +module.exports = (params, me) => + new Promise(async (res, rej) => +{ + // Get 'query' parameter + let query = params.query; + if (query === undefined || query === null || query.trim() === '') { + return rej('query is required'); + } + + // Get 'offset' parameter + let offset = params.offset; + if (offset !== undefined && offset !== null) { + offset = parseInt(offset, 10); + } else { + offset = 0; + } + + // Get 'max' parameter + let max = params.max; + if (max !== undefined && max !== null) { + max = parseInt(max, 10); + + // From 1 to 30 + if (!(1 <= max && max <= 30)) { + return rej('invalid max range'); + } + } else { + max = 10; + } + + // If Elasticsearch is available, search by it + // If not, search by MongoDB + (config.elasticsearch.enable ? byElasticsearch : byNative) + (res, rej, me, query, offset, max); +}); + +// Search by MongoDB +async function byNative(res, rej, me, query, offset, max) { + const escapedQuery = escapeRegexp(query); + + // Search posts + const posts = await Post + .find({ + text: new RegExp(escapedQuery) + }, { + sort: { + _id: -1 + }, + limit: max, + skip: offset + }) + .toArray(); + + // Serialize + res(await Promise.all(posts.map(async post => + await serialize(post, me)))); +} + +// Search by Elasticsearch +async function byElasticsearch(res, rej, me, query, offset, max) { + const es = require('../../db/elasticsearch'); + + es.search({ + index: 'misskey', + type: 'post', + body: { + size: max, + from: offset, + query: { + simple_query_string: { + fields: ['text'], + query: query, + default_operator: 'and' + } + }, + sort: [ + { _doc: 'desc' } + ], + highlight: { + pre_tags: [''], + post_tags: [''], + encoder: 'html', + fields: { + text: {} + } + } + } + }, async (error, response) => { + if (error) { + console.error(error); + return res(500); + } + + if (response.hits.total === 0) { + return res([]); + } + + const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id)); + + // Fetxh found posts + const posts = await Post + .find({ + _id: { + $in: hits + } + }, {}, { + sort: { + _id: -1 + } + }) + .toArray(); + + posts.map(post => { + post._highlight = response.hits.hits.filter(hit => post._id.equals(hit._id))[0].highlight.text[0]; + }); + + // Serialize + res(await Promise.all(posts.map(async post => + await serialize(post, me)))); + }); +} diff --git a/src/api/endpoints/posts/show.js b/src/api/endpoints/posts/show.js new file mode 100644 index 0000000000..19cdb74251 --- /dev/null +++ b/src/api/endpoints/posts/show.js @@ -0,0 +1,40 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Post from '../../models/post'; +import serialize from '../../serializers/post'; + +/** + * Show a post + * + * @param {Object} params + * @param {Object} user + * @return {Promise} + */ +module.exports = (params, user) => + new Promise(async (res, rej) => +{ + // Get 'post_id' parameter + const postId = params.post_id; + if (postId === undefined || postId === null) { + return rej('post_id is required'); + } + + // Get post + const post = await Post.findOne({ + _id: new mongo.ObjectID(postId) + }); + + if (post === null) { + return rej('post not found'); + } + + // Serialize + res(await serialize(post, user, { + serializeReplyTo: true, + includeIsLiked: true + })); +}); diff --git a/src/api/endpoints/posts/timeline.js b/src/api/endpoints/posts/timeline.js new file mode 100644 index 0000000000..489542da71 --- /dev/null +++ b/src/api/endpoints/posts/timeline.js @@ -0,0 +1,78 @@ +'use strict'; + +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import Post from '../../models/post'; +import getFriends from '../../common/get-friends'; +import serialize from '../../serializers/post'; + +/** + * Get timeline of myself + * + * @param {Object} params + * @param {Object} user + * @param {Object} app + * @return {Promise} + */ +module.exports = (params, user, app) => + new Promise(async (res, rej) => +{ + // Get 'limit' parameter + let limit = params.limit; + if (limit !== undefined && limit !== null) { + limit = parseInt(limit, 10); + + // From 1 to 100 + if (!(1 <= limit && limit <= 100)) { + return rej('invalid limit range'); + } + } else { + limit = 10; + } + + const since = params.since_id || null; + const max = params.max_id || null; + + // Check if both of since_id and max_id is specified + if (since !== null && max !== null) { + return rej('cannot set since_id and max_id'); + } + + // ID list of the user itself and other users who the user follows + const followingIds = await getFriends(user._id); + + // Construct query + const sort = { + _id: -1 + }; + const query = { + user_id: { + $in: followingIds + } + }; + if (since !== null) { + sort._id = 1; + query._id = { + $gt: new mongo.ObjectID(since) + }; + } else if (max !== null) { + query._id = { + $lt: new mongo.ObjectID(max) + }; + } + + // Issue query + const timeline = await Post + .find(query, {}, { + limit: limit, + sort: sort + }) + .toArray(); + + // Serialize + res(await Promise.all(timeline.map(async post => + await serialize(post, user) + ))); +}); -- cgit v1.2.3-freya