diff options
Diffstat (limited to 'src/api/endpoints/posts/search.ts')
| -rw-r--r-- | src/api/endpoints/posts/search.ts | 394 |
1 files changed, 315 insertions, 79 deletions
diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts index b434f64342..6e26f55390 100644 --- a/src/api/endpoints/posts/search.ts +++ b/src/api/endpoints/posts/search.ts @@ -1,12 +1,13 @@ /** * Module dependencies */ -import * as mongo from 'mongodb'; import $ from 'cafy'; const escapeRegexp = require('escape-regexp'); import Post from '../../models/post'; -import serialize from '../../serializers/post'; -import config from '../../../conf'; +import User from '../../models/user'; +import Mute from '../../models/mute'; +import getFriends from '../../common/get-friends'; +import { pack } from '../../models/post'; /** * Search a post @@ -16,104 +17,339 @@ import config from '../../../conf'; * @return {Promise<any>} */ module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'query' parameter - const [query, queryError] = $(params.query).string().pipe(x => x != '').$; - if (queryError) return rej('invalid query param'); + // Get 'text' parameter + const [text, textError] = $(params.text).optional.string().$; + if (textError) return rej('invalid text param'); + + // Get 'include_user_ids' parameter + const [includeUserIds = [], includeUserIdsErr] = $(params.include_user_ids).optional.array('id').$; + if (includeUserIdsErr) return rej('invalid include_user_ids param'); + + // Get 'exclude_user_ids' parameter + const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_user_ids).optional.array('id').$; + if (excludeUserIdsErr) return rej('invalid exclude_user_ids param'); + + // Get 'include_user_usernames' parameter + const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.include_user_usernames).optional.array('string').$; + if (includeUserUsernamesErr) return rej('invalid include_user_usernames param'); + + // Get 'exclude_user_usernames' parameter + const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.exclude_user_usernames).optional.array('string').$; + if (excludeUserUsernamesErr) return rej('invalid exclude_user_usernames 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 'since_date' parameter + const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$; + if (sinceDateErr) throw 'invalid since_date param'; + + // Get 'until_date' parameter + const [untilDate, untilDateErr] = $(params.until_date).optional.number().$; + if (untilDateErr) throw 'invalid until_date param'; // Get 'offset' parameter const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; if (offsetErr) return rej('invalid offset param'); - // Get 'max' parameter - const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$; - if (maxErr) return rej('invalid max 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({ + username_lower: username.toLowerCase() + }); + return _user ? _user._id : null; + }))).filter(id => id != null); + includeUsers = includeUsers.concat(ids); + } - // If Elasticsearch is available, search by $ - // If not, search by MongoDB - (config.elasticsearch.enable ? byElasticsearch : byNative) - (res, rej, me, query, offset, max); + let excludeUsers = excludeUserIds; + if (excludeUserUsernames != null) { + const ids = (await Promise.all(excludeUserUsernames.map(async (username) => { + const _user = await User.findOne({ + username_lower: 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); }); -// Search by MongoDB -async function byNative(res, rej, me, query, offset, max) { - const escapedQuery = escapeRegexp(query); +async function search( + res, rej, me, text, includeUserIds, excludeUserIds, following, + mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) { - // Search posts - const posts = await Post - .find({ - text: new RegExp(escapedQuery) - }, { - sort: { - _id: -1 - }, - limit: max, - skip: offset - }); + let q: any = { + $and: [] + }; - // Serialize - res(await Promise.all(posts.map(async post => - await serialize(post, me)))); -} + const push = x => q.$and.push(x); -// Search by Elasticsearch -async function byElasticsearch(res, rej, me, query, offset, max) { - const es = require('../../db/elasticsearch'); + if (text) { + // 完全一致検索 + if (/"""(.+?)"""/.test(text)) { + const x = text.match(/"""(.+?)"""/)[1]; + push({ + text: x + }); + } else { + push({ + $and: text.split(' ').map(x => ({ + // キーワードが-で始まる場合そのキーワードを除外する + text: x[0] == '-' ? { + $not: new RegExp(escapeRegexp(x.substr(1))) + } : new RegExp(escapeRegexp(x)) + })) + }); + } + } - 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: ['<mark>'], - post_tags: ['</mark>'], - encoder: 'html', - fields: { - text: {} - } + if (includeUserIds && includeUserIds.length != 0) { + push({ + user_id: { + $in: includeUserIds } + }); + } else if (excludeUserIds && excludeUserIds.length != 0) { + push({ + user_id: { + $nin: excludeUserIds + } + }); + } + + if (following != null && me != null) { + const ids = await getFriends(me._id, false); + push({ + user_id: following ? { + $in: ids + } : { + $nin: ids.concat(me._id) + } + }); + } + + if (me != null) { + const mutes = await Mute.find({ + muter_id: me._id, + deleted_at: { $exists: false } + }); + const mutedUserIds = mutes.map(m => m.mutee_id); + + switch (mute) { + case 'mute_all': + push({ + user_id: { + $nin: mutedUserIds + }, + '_reply.user_id': { + $nin: mutedUserIds + }, + '_repost.user_id': { + $nin: mutedUserIds + } + }); + break; + case 'mute_related': + push({ + '_reply.user_id': { + $nin: mutedUserIds + }, + '_repost.user_id': { + $nin: mutedUserIds + } + }); + break; + case 'mute_direct': + push({ + user_id: { + $nin: mutedUserIds + } + }); + break; + case 'direct_only': + push({ + user_id: { + $in: mutedUserIds + } + }); + break; + case 'related_only': + push({ + $or: [{ + '_reply.user_id': { + $in: mutedUserIds + } + }, { + '_repost.user_id': { + $in: mutedUserIds + } + }] + }); + break; + case 'all_only': + push({ + $or: [{ + user_id: { + $in: mutedUserIds + } + }, { + '_reply.user_id': { + $in: mutedUserIds + } + }, { + '_repost.user_id': { + $in: mutedUserIds + } + }] + }); + break; } - }, async (error, response) => { - if (error) { - console.error(error); - return res(500); - } + } - if (response.hits.total === 0) { - return res([]); + if (reply != null) { + if (reply) { + push({ + reply_id: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + reply_id: { + $exists: false + } + }, { + reply_id: null + }] + }); } + } - const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id)); + if (repost != null) { + if (repost) { + push({ + repost_id: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + repost_id: { + $exists: false + } + }, { + repost_id: null + }] + }); + } + } - // Fetch found posts - const posts = await Post - .find({ - _id: { - $in: hits + if (media != null) { + if (media) { + push({ + media_ids: { + $exists: true, + $ne: null } - }, { - sort: { - _id: -1 + }); + } else { + push({ + $or: [{ + media_ids: { + $exists: false + } + }, { + media_ids: null + }] + }); + } + } + + if (poll != null) { + if (poll) { + push({ + poll: { + $exists: true, + $ne: null } }); + } else { + push({ + $or: [{ + poll: { + $exists: false + } + }, { + poll: null + }] + }); + } + } + + if (sinceDate) { + push({ + created_at: { + $gt: new Date(sinceDate) + } + }); + } + + if (untilDate) { + push({ + created_at: { + $lt: new Date(untilDate) + } + }); + } + + if (q.$and.length == 0) { + q = {}; + } - posts.map(post => { - post._highlight = response.hits.hits.filter(hit => post._id.equals(hit._id))[0].highlight.text[0]; + // 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 serialize(post, me)))); - }); + // Serialize + res(await Promise.all(posts.map(async post => + await pack(post, me)))); } |