From 15dcfe16278ba9aaed34de7be0e917904534d72b Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 10 Jun 2018 12:19:19 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=8F=E3=83=83=E3=82=B7=E3=83=A5=E3=82=BF?= =?UTF-8?q?=E3=82=B0=E6=A4=9C=E7=B4=A2=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/app/desktop/script.ts | 2 + .../desktop/views/components/note-detail.vue | 2 +- .../desktop/views/components/notes.note.vue | 2 +- .../desktop/views/pages/deck/deck.note.vue | 2 +- src/client/app/desktop/views/pages/tag.vue | 128 +++++++ .../mobile/views/components/note-detail.vue | 2 +- .../app/mobile/views/components/note.vue | 2 +- src/models/note.ts | 1 + src/remote/activitypub/renderer/hashtag.ts | 2 +- src/server/api/endpoints.ts | 3 + .../api/endpoints/notes/search_by_tag.ts | 329 ++++++++++++++++++ src/text/html.ts | 2 +- 12 files changed, 470 insertions(+), 7 deletions(-) create mode 100644 src/client/app/desktop/views/pages/tag.vue create mode 100644 src/server/api/endpoints/notes/search_by_tag.ts diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 61f1f5b87..8b1623ce4 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -33,6 +33,7 @@ import MkHomeCustomize from './views/pages/home-customize.vue'; import MkMessagingRoom from './views/pages/messaging-room.vue'; import MkNote from './views/pages/note.vue'; import MkSearch from './views/pages/search.vue'; +import MkTag from './views/pages/tag.vue'; import MkOthello from './views/pages/othello.vue'; /** @@ -60,6 +61,7 @@ init(async (launch) => { { path: '/i/lists/:list', component: MkUserList }, { path: '/selectdrive', component: MkSelectDrive }, { path: '/search', component: MkSearch }, + { path: '/tags/:tag', component: MkTag }, { path: '/othello', component: MkOthello }, { path: '/othello/:game', component: MkOthello }, { path: '/@:user', component: MkUser }, diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index 2f28d223d..4b5e5bebd 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -48,7 +48,7 @@
- {{ tag }} + {{ tag }}
%fa:map-marker-alt% %i18n:@location%
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index 2f185e335..ee11fcc55 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -33,7 +33,7 @@
- {{ tag }} + {{ tag }}
%fa:map-marker-alt% 位置情報
diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue index a888ea7b0..bf830b92e 100644 --- a/src/client/app/desktop/views/pages/deck/deck.note.vue +++ b/src/client/app/desktop/views/pages/deck/deck.note.vue @@ -33,7 +33,7 @@
- {{ tag }} + {{ tag }}
%fa:map-marker-alt% %i18n:@location%
diff --git a/src/client/app/desktop/views/pages/tag.vue b/src/client/app/desktop/views/pages/tag.vue new file mode 100644 index 000000000..0b8fd81ac --- /dev/null +++ b/src/client/app/desktop/views/pages/tag.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue index bdbb8876d..f3e77d706 100644 --- a/src/client/app/mobile/views/components/note-detail.vue +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -41,7 +41,7 @@
- {{ tag }} + {{ tag }}
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index 62cee0abf..4498bb563 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -33,7 +33,7 @@
- {{ tag }} + {{ tag }}
%fa:map-marker-alt% %i18n:@location% diff --git a/src/models/note.ts b/src/models/note.ts index d4681b7b7..359d95373 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -16,6 +16,7 @@ import Following from './following'; const Note = db.get('notes'); Note.createIndex('uri', { sparse: true, unique: true }); Note.createIndex('userId'); +Note.createIndex('tags', { sparse: true }); Note.createIndex({ createdAt: -1 }); diff --git a/src/remote/activitypub/renderer/hashtag.ts b/src/remote/activitypub/renderer/hashtag.ts index cf0b07b48..50761c868 100644 --- a/src/remote/activitypub/renderer/hashtag.ts +++ b/src/remote/activitypub/renderer/hashtag.ts @@ -2,6 +2,6 @@ import config from '../../../config'; export default tag => ({ type: 'Hashtag', - href: `${config.url}/search?q=#${encodeURIComponent(tag)}`, + href: `${config.url}/tags/${encodeURIComponent(tag)}`, name: '#' + tag }); diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index 94e649d29..91e5298e7 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -525,6 +525,9 @@ const endpoints: Endpoint[] = [ { name: 'notes/search' }, + { + name: 'notes/search_by_tag' + }, { name: 'notes/timeline', withCredential: true, diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts new file mode 100644 index 000000000..4cf070f4c --- /dev/null +++ b/src/server/api/endpoints/notes/search_by_tag.ts @@ -0,0 +1,329 @@ +import $ from 'cafy'; import ID from '../../../../cafy-id'; +import Note from '../../../../models/note'; +import User from '../../../../models/user'; +import Mute from '../../../../models/mute'; +import { getFriendIds } from '../../common/get-friends'; +import { pack } from '../../../../models/note'; + +/** + * Search notes by tag + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'tag' parameter + const [tag, tagError] = $.str.get(params.tag); + if (tagError) return rej('invalid tag param'); + + // Get 'includeUserIds' parameter + const [includeUserIds = [], includeUserIdsErr] = $.arr($.type(ID)).optional().get(params.includeUserIds); + if (includeUserIdsErr) return rej('invalid includeUserIds param'); + + // Get 'excludeUserIds' parameter + const [excludeUserIds = [], excludeUserIdsErr] = $.arr($.type(ID)).optional().get(params.excludeUserIds); + if (excludeUserIdsErr) return rej('invalid excludeUserIds param'); + + // Get 'includeUserUsernames' parameter + const [includeUserUsernames = [], includeUserUsernamesErr] = $.arr($.str).optional().get(params.includeUserUsernames); + if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param'); + + // Get 'excludeUserUsernames' parameter + const [excludeUserUsernames = [], excludeUserUsernamesErr] = $.arr($.str).optional().get(params.excludeUserUsernames); + if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param'); + + // Get 'following' parameter + const [following = null, followingErr] = $.bool.optional().nullable().get(params.following); + if (followingErr) return rej('invalid following param'); + + // Get 'mute' parameter + const [mute = 'mute_all', muteErr] = $.str.optional().get(params.mute); + if (muteErr) return rej('invalid mute param'); + + // Get 'reply' parameter + const [reply = null, replyErr] = $.bool.optional().nullable().get(params.reply); + if (replyErr) return rej('invalid reply param'); + + // Get 'renote' parameter + const [renote = null, renoteErr] = $.bool.optional().nullable().get(params.renote); + if (renoteErr) return rej('invalid renote param'); + + // Get 'media' parameter + const [media = null, mediaErr] = $.bool.optional().nullable().get(params.media); + if (mediaErr) return rej('invalid media param'); + + // Get 'poll' parameter + const [poll = null, pollErr] = $.bool.optional().nullable().get(params.poll); + if (pollErr) return rej('invalid poll param'); + + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $.num.optional().get(params.sinceDate); + if (sinceDateErr) throw 'invalid sinceDate param'; + + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $.num.optional().get(params.untilDate); + if (untilDateErr) throw 'invalid untilDate param'; + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $.num.optional().min(0).get(params.offset); + if (offsetErr) return rej('invalid offset param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $.num.optional().range(1, 30).get(params.limit); + 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, tag, includeUsers, excludeUsers, following, + mute, reply, renote, media, poll, sinceDate, untilDate, offset, limit); +}); + +async function search( + res, rej, me, tag, includeUserIds, excludeUserIds, following, + mute, reply, renote, media, poll, sinceDate, untilDate, offset, max) { + + let q: any = { + $and: [{ + tags: tag + }] + }; + + const push = x => q.$and.push(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 getFriendIds(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(note => pack(note, me)))); +} diff --git a/src/text/html.ts b/src/text/html.ts index 53d4e8a52..70b341689 100644 --- a/src/text/html.ts +++ b/src/text/html.ts @@ -25,7 +25,7 @@ const handlers = { hashtag({ document }, { hashtag }) { const a = document.createElement('a'); - a.href = config.url + '/search?q=#' + hashtag; + a.href = config.url + '/tags/' + hashtag; a.textContent = '#' + hashtag; a.setAttribute('rel', 'tag'); document.body.appendChild(a);