diff options
Diffstat (limited to 'server/controllers/api/search.ts')
-rw-r--r-- | server/controllers/api/search.ts | 294 |
1 files changed, 0 insertions, 294 deletions
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts deleted file mode 100644 index c975c5c3c..000000000 --- a/server/controllers/api/search.ts +++ /dev/null | |||
@@ -1,294 +0,0 @@ | |||
1 | import * as express from 'express' | ||
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | ||
3 | import { doJSONRequest } from '@server/helpers/requests' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
7 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' | ||
8 | import { getServerActor } from '@server/models/application/application' | ||
9 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
11 | import { ResultList, Video, VideoChannel } from '@shared/models' | ||
12 | import { SearchTargetQuery } from '@shared/models/search/search-target-query.model' | ||
13 | import { VideoChannelsSearchQuery, VideosSearchQuery } from '../../../shared/models/search' | ||
14 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | ||
15 | import { logger } from '../../helpers/logger' | ||
16 | import { getFormattedObjects } from '../../helpers/utils' | ||
17 | import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../../lib/activitypub/actors' | ||
18 | import { | ||
19 | asyncMiddleware, | ||
20 | commonVideosFiltersValidator, | ||
21 | openapiOperationDoc, | ||
22 | optionalAuthenticate, | ||
23 | paginationValidator, | ||
24 | setDefaultPagination, | ||
25 | setDefaultSearchSort, | ||
26 | videoChannelsListSearchValidator, | ||
27 | videoChannelsSearchSortValidator, | ||
28 | videosSearchSortValidator, | ||
29 | videosSearchValidator | ||
30 | } from '../../middlewares' | ||
31 | import { VideoModel } from '../../models/video/video' | ||
32 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
33 | import { MChannelAccountDefault, MVideoAccountLightBlacklistAllFiles } from '../../types/models' | ||
34 | |||
35 | const searchRouter = express.Router() | ||
36 | |||
37 | searchRouter.get('/videos', | ||
38 | openapiOperationDoc({ operationId: 'searchVideos' }), | ||
39 | paginationValidator, | ||
40 | setDefaultPagination, | ||
41 | videosSearchSortValidator, | ||
42 | setDefaultSearchSort, | ||
43 | optionalAuthenticate, | ||
44 | commonVideosFiltersValidator, | ||
45 | videosSearchValidator, | ||
46 | asyncMiddleware(searchVideos) | ||
47 | ) | ||
48 | |||
49 | searchRouter.get('/video-channels', | ||
50 | openapiOperationDoc({ operationId: 'searchChannels' }), | ||
51 | paginationValidator, | ||
52 | setDefaultPagination, | ||
53 | videoChannelsSearchSortValidator, | ||
54 | setDefaultSearchSort, | ||
55 | optionalAuthenticate, | ||
56 | videoChannelsListSearchValidator, | ||
57 | asyncMiddleware(searchVideoChannels) | ||
58 | ) | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | export { searchRouter } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | function searchVideoChannels (req: express.Request, res: express.Response) { | ||
67 | const query: VideoChannelsSearchQuery = req.query | ||
68 | const search = query.search | ||
69 | |||
70 | const isURISearch = search.startsWith('http://') || search.startsWith('https://') | ||
71 | |||
72 | const parts = search.split('@') | ||
73 | |||
74 | // Handle strings like @toto@example.com | ||
75 | if (parts.length === 3 && parts[0].length === 0) parts.shift() | ||
76 | const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' ')) | ||
77 | |||
78 | if (isURISearch || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) | ||
79 | |||
80 | // @username -> username to search in DB | ||
81 | if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '') | ||
82 | |||
83 | if (isSearchIndexSearch(query)) { | ||
84 | return searchVideoChannelsIndex(query, res) | ||
85 | } | ||
86 | |||
87 | return searchVideoChannelsDB(query, res) | ||
88 | } | ||
89 | |||
90 | async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { | ||
91 | const result = await buildMutedForSearchIndex(res) | ||
92 | |||
93 | const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params') | ||
94 | |||
95 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' | ||
96 | |||
97 | try { | ||
98 | logger.debug('Doing video channels search index request on %s.', url, { body }) | ||
99 | |||
100 | const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body }) | ||
101 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result') | ||
102 | |||
103 | return res.json(jsonResult) | ||
104 | } catch (err) { | ||
105 | logger.warn('Cannot use search index to make video channels search.', { err }) | ||
106 | |||
107 | return res.fail({ | ||
108 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
109 | message: 'Cannot use search index to make video channels search' | ||
110 | }) | ||
111 | } | ||
112 | } | ||
113 | |||
114 | async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { | ||
115 | const serverActor = await getServerActor() | ||
116 | |||
117 | const apiOptions = await Hooks.wrapObject({ | ||
118 | actorId: serverActor.id, | ||
119 | search: query.search, | ||
120 | start: query.start, | ||
121 | count: query.count, | ||
122 | sort: query.sort | ||
123 | }, 'filter:api.search.video-channels.local.list.params') | ||
124 | |||
125 | const resultList = await Hooks.wrapPromiseFun( | ||
126 | VideoChannelModel.searchForApi, | ||
127 | apiOptions, | ||
128 | 'filter:api.search.video-channels.local.list.result' | ||
129 | ) | ||
130 | |||
131 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
132 | } | ||
133 | |||
134 | async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) { | ||
135 | let videoChannel: MChannelAccountDefault | ||
136 | let uri = search | ||
137 | |||
138 | if (isWebfingerSearch) { | ||
139 | try { | ||
140 | uri = await loadActorUrlOrGetFromWebfinger(search) | ||
141 | } catch (err) { | ||
142 | logger.warn('Cannot load actor URL or get from webfinger.', { search, err }) | ||
143 | |||
144 | return res.json({ total: 0, data: [] }) | ||
145 | } | ||
146 | } | ||
147 | |||
148 | if (isUserAbleToSearchRemoteURI(res)) { | ||
149 | try { | ||
150 | const actor = await getOrCreateAPActor(uri, 'all', true, true) | ||
151 | videoChannel = actor.VideoChannel | ||
152 | } catch (err) { | ||
153 | logger.info('Cannot search remote video channel %s.', uri, { err }) | ||
154 | } | ||
155 | } else { | ||
156 | videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(uri) | ||
157 | } | ||
158 | |||
159 | return res.json({ | ||
160 | total: videoChannel ? 1 : 0, | ||
161 | data: videoChannel ? [ videoChannel.toFormattedJSON() ] : [] | ||
162 | }) | ||
163 | } | ||
164 | |||
165 | function searchVideos (req: express.Request, res: express.Response) { | ||
166 | const query: VideosSearchQuery = req.query | ||
167 | const search = query.search | ||
168 | |||
169 | if (search && (search.startsWith('http://') || search.startsWith('https://'))) { | ||
170 | return searchVideoURI(search, res) | ||
171 | } | ||
172 | |||
173 | if (isSearchIndexSearch(query)) { | ||
174 | return searchVideosIndex(query, res) | ||
175 | } | ||
176 | |||
177 | return searchVideosDB(query, res) | ||
178 | } | ||
179 | |||
180 | async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { | ||
181 | const result = await buildMutedForSearchIndex(res) | ||
182 | |||
183 | let body: VideosSearchQuery = Object.assign(query, result) | ||
184 | |||
185 | // Use the default instance NSFW policy if not specified | ||
186 | if (!body.nsfw) { | ||
187 | const nsfwPolicy = res.locals.oauth | ||
188 | ? res.locals.oauth.token.User.nsfwPolicy | ||
189 | : CONFIG.INSTANCE.DEFAULT_NSFW_POLICY | ||
190 | |||
191 | body.nsfw = nsfwPolicy === 'do_not_list' | ||
192 | ? 'false' | ||
193 | : 'both' | ||
194 | } | ||
195 | |||
196 | body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params') | ||
197 | |||
198 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' | ||
199 | |||
200 | try { | ||
201 | logger.debug('Doing videos search index request on %s.', url, { body }) | ||
202 | |||
203 | const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body }) | ||
204 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result') | ||
205 | |||
206 | return res.json(jsonResult) | ||
207 | } catch (err) { | ||
208 | logger.warn('Cannot use search index to make video search.', { err }) | ||
209 | |||
210 | return res.fail({ | ||
211 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
212 | message: 'Cannot use search index to make video search' | ||
213 | }) | ||
214 | } | ||
215 | } | ||
216 | |||
217 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { | ||
218 | const apiOptions = await Hooks.wrapObject(Object.assign(query, { | ||
219 | includeLocalVideos: true, | ||
220 | nsfw: buildNSFWFilter(res, query.nsfw), | ||
221 | filter: query.filter, | ||
222 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
223 | }), 'filter:api.search.videos.local.list.params') | ||
224 | |||
225 | const resultList = await Hooks.wrapPromiseFun( | ||
226 | VideoModel.searchAndPopulateAccountAndServer, | ||
227 | apiOptions, | ||
228 | 'filter:api.search.videos.local.list.result' | ||
229 | ) | ||
230 | |||
231 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
232 | } | ||
233 | |||
234 | async function searchVideoURI (url: string, res: express.Response) { | ||
235 | let video: MVideoAccountLightBlacklistAllFiles | ||
236 | |||
237 | // Check if we can fetch a remote video with the URL | ||
238 | if (isUserAbleToSearchRemoteURI(res)) { | ||
239 | try { | ||
240 | const syncParam = { | ||
241 | likes: false, | ||
242 | dislikes: false, | ||
243 | shares: false, | ||
244 | comments: false, | ||
245 | thumbnail: true, | ||
246 | refreshVideo: false | ||
247 | } | ||
248 | |||
249 | const result = await getOrCreateAPVideo({ videoObject: url, syncParam }) | ||
250 | video = result ? result.video : undefined | ||
251 | } catch (err) { | ||
252 | logger.info('Cannot search remote video %s.', url, { err }) | ||
253 | } | ||
254 | } else { | ||
255 | video = await VideoModel.loadByUrlAndPopulateAccount(url) | ||
256 | } | ||
257 | |||
258 | return res.json({ | ||
259 | total: video ? 1 : 0, | ||
260 | data: video ? [ video.toFormattedJSON() ] : [] | ||
261 | }) | ||
262 | } | ||
263 | |||
264 | function isSearchIndexSearch (query: SearchTargetQuery) { | ||
265 | if (query.searchTarget === 'search-index') return true | ||
266 | |||
267 | const searchIndexConfig = CONFIG.SEARCH.SEARCH_INDEX | ||
268 | |||
269 | if (searchIndexConfig.ENABLED !== true) return false | ||
270 | |||
271 | if (searchIndexConfig.DISABLE_LOCAL_SEARCH) return true | ||
272 | if (searchIndexConfig.IS_DEFAULT_SEARCH && !query.searchTarget) return true | ||
273 | |||
274 | return false | ||
275 | } | ||
276 | |||
277 | async function buildMutedForSearchIndex (res: express.Response) { | ||
278 | const serverActor = await getServerActor() | ||
279 | const accountIds = [ serverActor.Account.id ] | ||
280 | |||
281 | if (res.locals.oauth) { | ||
282 | accountIds.push(res.locals.oauth.token.User.Account.id) | ||
283 | } | ||
284 | |||
285 | const [ blockedHosts, blockedAccounts ] = await Promise.all([ | ||
286 | ServerBlocklistModel.listHostsBlockedBy(accountIds), | ||
287 | AccountBlocklistModel.listHandlesBlockedBy(accountIds) | ||
288 | ]) | ||
289 | |||
290 | return { | ||
291 | blockedHosts, | ||
292 | blockedAccounts | ||
293 | } | ||
294 | } | ||