diff options
-rw-r--r-- | client/src/app/search/search.component.scss | 2 | ||||
-rw-r--r-- | client/src/assets/images/menu/podcasts.svg | 26 | ||||
-rw-r--r-- | server/controllers/api/accounts.ts | 4 | ||||
-rw-r--r-- | server/controllers/api/search.ts | 11 | ||||
-rw-r--r-- | server/controllers/api/video-channel.ts | 4 | ||||
-rw-r--r-- | server/helpers/express-utils.ts | 8 | ||||
-rw-r--r-- | server/lib/activitypub/actor.ts | 31 | ||||
-rw-r--r-- | server/models/video/video.ts | 63 | ||||
-rw-r--r-- | server/tests/api/search/search-activitypub-video-channels.ts | 52 |
9 files changed, 118 insertions, 83 deletions
diff --git a/client/src/app/search/search.component.scss b/client/src/app/search/search.component.scss index be7dd39cf..e5dfddcc5 100644 --- a/client/src/app/search/search.component.scss +++ b/client/src/app/search/search.component.scss | |||
@@ -113,8 +113,6 @@ | |||
113 | } | 113 | } |
114 | 114 | ||
115 | .video-channel-info { | 115 | .video-channel-info { |
116 | |||
117 | |||
118 | flex-grow: 1; | 116 | flex-grow: 1; |
119 | width: fit-content; | 117 | width: fit-content; |
120 | 118 | ||
diff --git a/client/src/assets/images/menu/podcasts.svg b/client/src/assets/images/menu/podcasts.svg deleted file mode 100644 index cd6efc54e..000000000 --- a/client/src/assets/images/menu/podcasts.svg +++ /dev/null | |||
@@ -1,26 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>podcasts</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs> | ||
7 | <linearGradient x1="50%" y1="0%" x2="50%" y2="97.3333865%" id="linearGradient-1"> | ||
8 | <stop stop-color="#808080" offset="0%"></stop> | ||
9 | <stop stop-color="#808080" stop-opacity="0.247310915" offset="100%"></stop> | ||
10 | </linearGradient> | ||
11 | <linearGradient x1="50%" y1="0%" x2="50%" y2="97.8635204%" id="linearGradient-2"> | ||
12 | <stop stop-color="#808080" offset="0%"></stop> | ||
13 | <stop stop-color="#808080" stop-opacity="0.250707654" offset="100%"></stop> | ||
14 | </linearGradient> | ||
15 | </defs> | ||
16 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
17 | <g id="Artboard-4" transform="translate(-532.000000, -775.000000)"> | ||
18 | <g id="312" transform="translate(532.000000, 775.000000)"> | ||
19 | <circle id="Oval-169" fill="#808080" cx="12" cy="10" r="3"></circle> | ||
20 | <path d="M16.3851456,13.8501206 C17.4222377,12.6991612 18,11.4167199 18,10 C18,6.74089158 15.2591084,4 12,4 C8.74089158,4 6,6.74089158 6,10 C6,11.4186069 6.57916224,12.7027674 7.61838071,13.8540306 C7.80341316,14.0590125 8.11958231,14.0751848 8.32456427,13.8901523 C8.52954623,13.7051199 8.5457185,13.3889507 8.36068606,13.1839688 C7.47616718,12.2040844 7,11.148292 7,10 C7,7.29317633 9.29317633,5 12,5 C14.7068237,5 17,7.29317633 17,10 C17,11.1466944 16.5249958,12.2010466 15.6422459,13.1807178 C15.4573954,13.3858639 15.4738483,13.7020185 15.6789944,13.886869 C15.8841405,14.0717195 16.2002951,14.0552666 16.3851456,13.8501206 Z" id="Oval-169" fill="url(#linearGradient-1)" fill-rule="nonzero"></path> | ||
21 | <path d="M17.5678226,18.3077078 C20.3159646,16.4626239 22,13.3733223 22,10 C22,4.4771525 17.5228475,0 12,0 C6.4771525,0 2,4.4771525 2,10 C2,13.3762414 3.68696556,16.4678678 6.43901638,18.3122954 C6.89779529,18.6197696 7.51896613,18.4971129 7.82644029,18.0383339 C8.13391444,17.579555 8.0112577,16.9583842 7.55247879,16.65091 C5.34877306,15.1739839 4,12.7021478 4,10 C4,5.581722 7.581722,2 12,2 C16.418278,2 20,5.581722 20,10 C20,12.699815 18.6535741,15.1697843 16.4529947,16.6472384 C15.9944687,16.9550897 15.8723227,17.5763611 16.180174,18.0348871 C16.4880252,18.4934131 17.1092967,18.6155591 17.5678226,18.3077078 Z" id="Oval-169" fill="url(#linearGradient-2)" fill-rule="nonzero"></path> | ||
22 | <path d="M9.32918137,15.9750882 C9.14737952,14.8842771 9.89826062,14 10.9979131,14 L13.0020869,14 C14.1055038,14 14.8534426,14.8793447 14.6708186,15.9750882 L13.6633817,22.0197096 C13.5731485,22.561109 13.0573397,23 12.5010434,23 L11.4989566,23 C10.9472481,23 10.4276519,22.5659113 10.3366183,22.0197096 L9.32918137,15.9750882 Z" id="Rectangle-217" fill="#808080"></path> | ||
23 | </g> | ||
24 | </g> | ||
25 | </g> | ||
26 | </svg> | ||
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 7b7e5e740..b7691ccba 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | import { accountsNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators' | 11 | import { accountsNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators' |
12 | import { AccountModel } from '../../models/account/account' | 12 | import { AccountModel } from '../../models/account/account' |
13 | import { VideoModel } from '../../models/video/video' | 13 | import { VideoModel } from '../../models/video/video' |
14 | import { buildNSFWFilter } from '../../helpers/express-utils' | 14 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
15 | import { VideoChannelModel } from '../../models/video/video-channel' | 15 | import { VideoChannelModel } from '../../models/video/video-channel' |
16 | 16 | ||
17 | const accountsRouter = express.Router() | 17 | const accountsRouter = express.Router() |
@@ -73,8 +73,10 @@ async function listVideoAccountChannels (req: express.Request, res: express.Resp | |||
73 | 73 | ||
74 | async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 74 | async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
75 | const account: AccountModel = res.locals.account | 75 | const account: AccountModel = res.locals.account |
76 | const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | ||
76 | 77 | ||
77 | const resultList = await VideoModel.listForApi({ | 78 | const resultList = await VideoModel.listForApi({ |
79 | actorId, | ||
78 | start: req.query.start, | 80 | start: req.query.start, |
79 | count: req.query.count, | 81 | count: req.query.count, |
80 | sort: req.query.sort, | 82 | sort: req.query.sort, |
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts index 959d79855..bb7174891 100644 --- a/server/controllers/api/search.ts +++ b/server/controllers/api/search.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { buildNSFWFilter } from '../../helpers/express-utils' | 2 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
3 | import { getFormattedObjects, getServerActor } from '../../helpers/utils' | 3 | import { getFormattedObjects, getServerActor } from '../../helpers/utils' |
4 | import { VideoModel } from '../../models/video/video' | 4 | import { VideoModel } from '../../models/video/video' |
5 | import { | 5 | import { |
@@ -88,7 +88,7 @@ async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean | |||
88 | 88 | ||
89 | if (isUserAbleToSearchRemoteURI(res)) { | 89 | if (isUserAbleToSearchRemoteURI(res)) { |
90 | try { | 90 | try { |
91 | const actor = await getOrCreateActorAndServerAndModel(uri) | 91 | const actor = await getOrCreateActorAndServerAndModel(uri, true, true) |
92 | videoChannel = actor.VideoChannel | 92 | videoChannel = actor.VideoChannel |
93 | } catch (err) { | 93 | } catch (err) { |
94 | logger.info('Cannot search remote video channel %s.', uri, { err }) | 94 | logger.info('Cannot search remote video channel %s.', uri, { err }) |
@@ -152,10 +152,3 @@ async function searchVideoURI (url: string, res: express.Response) { | |||
152 | data: video ? [ video.toFormattedJSON() ] : [] | 152 | data: video ? [ video.toFormattedJSON() ] : [] |
153 | }) | 153 | }) |
154 | } | 154 | } |
155 | |||
156 | function isUserAbleToSearchRemoteURI (res: express.Response) { | ||
157 | const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
158 | |||
159 | return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || | ||
160 | (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) | ||
161 | } | ||
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index bd08d7a08..a7a36080b 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -19,7 +19,7 @@ import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../m | |||
19 | import { sendUpdateActor } from '../../lib/activitypub/send' | 19 | import { sendUpdateActor } from '../../lib/activitypub/send' |
20 | import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' | 20 | import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' |
21 | import { createVideoChannel } from '../../lib/video-channel' | 21 | import { createVideoChannel } from '../../lib/video-channel' |
22 | import { buildNSFWFilter, createReqFiles } from '../../helpers/express-utils' | 22 | import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
23 | import { setAsyncActorKeys } from '../../lib/activitypub' | 23 | import { setAsyncActorKeys } from '../../lib/activitypub' |
24 | import { AccountModel } from '../../models/account/account' | 24 | import { AccountModel } from '../../models/account/account' |
25 | import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' | 25 | import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' |
@@ -210,8 +210,10 @@ async function getVideoChannel (req: express.Request, res: express.Response, nex | |||
210 | 210 | ||
211 | async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 211 | async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
212 | const videoChannelInstance: VideoChannelModel = res.locals.videoChannel | 212 | const videoChannelInstance: VideoChannelModel = res.locals.videoChannel |
213 | const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | ||
213 | 214 | ||
214 | const resultList = await VideoModel.listForApi({ | 215 | const resultList = await VideoModel.listForApi({ |
216 | actorId, | ||
215 | start: req.query.start, | 217 | start: req.query.start, |
216 | count: req.query.count, | 218 | count: req.query.count, |
217 | sort: req.query.sort, | 219 | sort: req.query.sort, |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index 1d7bee87e..b715fb7d0 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -95,11 +95,19 @@ function createReqFiles ( | |||
95 | return multer({ storage }).fields(fields) | 95 | return multer({ storage }).fields(fields) |
96 | } | 96 | } |
97 | 97 | ||
98 | function isUserAbleToSearchRemoteURI (res: express.Response) { | ||
99 | const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
100 | |||
101 | return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || | ||
102 | (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) | ||
103 | } | ||
104 | |||
98 | // --------------------------------------------------------------------------- | 105 | // --------------------------------------------------------------------------- |
99 | 106 | ||
100 | export { | 107 | export { |
101 | buildNSFWFilter, | 108 | buildNSFWFilter, |
102 | getHostWithPort, | 109 | getHostWithPort, |
110 | isUserAbleToSearchRemoteURI, | ||
103 | badRequest, | 111 | badRequest, |
104 | createReqFiles, | 112 | createReqFiles, |
105 | cleanUpReqFiles | 113 | cleanUpReqFiles |
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 22e1c9f19..1657262d7 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -36,8 +36,13 @@ function setAsyncActorKeys (actor: ActorModel) { | |||
36 | }) | 36 | }) |
37 | } | 37 | } |
38 | 38 | ||
39 | async function getOrCreateActorAndServerAndModel (activityActor: string | ActivityPubActor, recurseIfNeeded = true) { | 39 | async function getOrCreateActorAndServerAndModel ( |
40 | activityActor: string | ActivityPubActor, | ||
41 | recurseIfNeeded = true, | ||
42 | updateCollections = false | ||
43 | ) { | ||
40 | const actorUrl = getActorUrl(activityActor) | 44 | const actorUrl = getActorUrl(activityActor) |
45 | let created = false | ||
41 | 46 | ||
42 | let actor = await ActorModel.loadByUrl(actorUrl) | 47 | let actor = await ActorModel.loadByUrl(actorUrl) |
43 | // Orphan actor (not associated to an account of channel) so recreate it | 48 | // Orphan actor (not associated to an account of channel) so recreate it |
@@ -68,15 +73,21 @@ async function getOrCreateActorAndServerAndModel (activityActor: string | Activi | |||
68 | } | 73 | } |
69 | 74 | ||
70 | actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor) | 75 | actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor) |
76 | created = true | ||
71 | } | 77 | } |
72 | 78 | ||
73 | if (actor.Account) actor.Account.Actor = actor | 79 | if (actor.Account) actor.Account.Actor = actor |
74 | if (actor.VideoChannel) actor.VideoChannel.Actor = actor | 80 | if (actor.VideoChannel) actor.VideoChannel.Actor = actor |
75 | 81 | ||
76 | actor = await retryTransactionWrapper(refreshActorIfNeeded, actor) | 82 | const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor) |
77 | if (!actor) throw new Error('Actor ' + actor.url + ' does not exist anymore.') | 83 | if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.') |
78 | 84 | ||
79 | return actor | 85 | if ((created === true || refreshed === true) && updateCollections === true) { |
86 | const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' } | ||
87 | await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | ||
88 | } | ||
89 | |||
90 | return actorRefreshed | ||
80 | } | 91 | } |
81 | 92 | ||
82 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) { | 93 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) { |
@@ -359,8 +370,8 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu | |||
359 | return videoChannelCreated | 370 | return videoChannelCreated |
360 | } | 371 | } |
361 | 372 | ||
362 | async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> { | 373 | async function refreshActorIfNeeded (actor: ActorModel): Promise<{ actor: ActorModel, refreshed: boolean }> { |
363 | if (!actor.isOutdated()) return actor | 374 | if (!actor.isOutdated()) return { actor, refreshed: false } |
364 | 375 | ||
365 | try { | 376 | try { |
366 | const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) | 377 | const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) |
@@ -369,12 +380,12 @@ async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> { | |||
369 | if (statusCode === 404) { | 380 | if (statusCode === 404) { |
370 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | 381 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) |
371 | actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() | 382 | actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() |
372 | return undefined | 383 | return { actor: undefined, refreshed: false } |
373 | } | 384 | } |
374 | 385 | ||
375 | if (result === undefined) { | 386 | if (result === undefined) { |
376 | logger.warn('Cannot fetch remote actor in refresh actor.') | 387 | logger.warn('Cannot fetch remote actor in refresh actor.') |
377 | return actor | 388 | return { actor, refreshed: false } |
378 | } | 389 | } |
379 | 390 | ||
380 | return sequelizeTypescript.transaction(async t => { | 391 | return sequelizeTypescript.transaction(async t => { |
@@ -403,10 +414,10 @@ async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> { | |||
403 | await actor.VideoChannel.save({ transaction: t }) | 414 | await actor.VideoChannel.save({ transaction: t }) |
404 | } | 415 | } |
405 | 416 | ||
406 | return actor | 417 | return { refreshed: true, actor } |
407 | }) | 418 | }) |
408 | } catch (err) { | 419 | } catch (err) { |
409 | logger.warn('Cannot refresh actor.', { err }) | 420 | logger.warn('Cannot refresh actor.', { err }) |
410 | return actor | 421 | return { actor, refreshed: false } |
411 | } | 422 | } |
412 | } | 423 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 7acbc60f7..a956da16e 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -201,39 +201,12 @@ type AvailableForListOptions = { | |||
201 | ] | 201 | ] |
202 | } | 202 | } |
203 | 203 | ||
204 | // Force actorId to be a number to avoid SQL injections | ||
205 | const actorIdNumber = parseInt(options.actorId.toString(), 10) | ||
206 | let localVideosReq = '' | ||
207 | if (options.includeLocalVideos === true) { | ||
208 | localVideosReq = ' UNION ALL ' + | ||
209 | 'SELECT "video"."id" AS "id" FROM "video" ' + | ||
210 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
211 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + | ||
212 | 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + | ||
213 | 'WHERE "actor"."serverId" IS NULL' | ||
214 | } | ||
215 | |||
216 | // FIXME: It would be more efficient to use a CTE so we join AFTER the filters, but sequelize does not support it... | 204 | // FIXME: It would be more efficient to use a CTE so we join AFTER the filters, but sequelize does not support it... |
217 | const query: IFindOptions<VideoModel> = { | 205 | const query: IFindOptions<VideoModel> = { |
218 | where: { | 206 | where: { |
219 | id: { | 207 | id: { |
220 | [Sequelize.Op.notIn]: Sequelize.literal( | 208 | [Sequelize.Op.notIn]: Sequelize.literal( |
221 | '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' | 209 | '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' |
222 | ), | ||
223 | [ Sequelize.Op.in ]: Sequelize.literal( | ||
224 | '(' + | ||
225 | 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + | ||
226 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + | ||
227 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | ||
228 | ' UNION ALL ' + | ||
229 | 'SELECT "video"."id" AS "id" FROM "video" ' + | ||
230 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
231 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + | ||
232 | 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + | ||
233 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' + | ||
234 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | ||
235 | localVideosReq + | ||
236 | ')' | ||
237 | ) | 210 | ) |
238 | }, | 211 | }, |
239 | // Always list public videos | 212 | // Always list public videos |
@@ -254,6 +227,36 @@ type AvailableForListOptions = { | |||
254 | include: [ videoChannelInclude ] | 227 | include: [ videoChannelInclude ] |
255 | } | 228 | } |
256 | 229 | ||
230 | if (options.actorId) { | ||
231 | let localVideosReq = '' | ||
232 | if (options.includeLocalVideos === true) { | ||
233 | localVideosReq = ' UNION ALL ' + | ||
234 | 'SELECT "video"."id" AS "id" FROM "video" ' + | ||
235 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
236 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + | ||
237 | 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + | ||
238 | 'WHERE "actor"."serverId" IS NULL' | ||
239 | } | ||
240 | |||
241 | // Force actorId to be a number to avoid SQL injections | ||
242 | const actorIdNumber = parseInt(options.actorId.toString(), 10) | ||
243 | query.where['id'][ Sequelize.Op.in ] = Sequelize.literal( | ||
244 | '(' + | ||
245 | 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + | ||
246 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + | ||
247 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | ||
248 | ' UNION ALL ' + | ||
249 | 'SELECT "video"."id" AS "id" FROM "video" ' + | ||
250 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
251 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + | ||
252 | 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + | ||
253 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' + | ||
254 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | ||
255 | localVideosReq + | ||
256 | ')' | ||
257 | ) | ||
258 | } | ||
259 | |||
257 | if (options.withFiles === true) { | 260 | if (options.withFiles === true) { |
258 | query.include.push({ | 261 | query.include.push({ |
259 | model: VideoFileModel.unscoped(), | 262 | model: VideoFileModel.unscoped(), |
@@ -849,7 +852,8 @@ export class VideoModel extends Model<VideoModel> { | |||
849 | order: getSort(options.sort) | 852 | order: getSort(options.sort) |
850 | } | 853 | } |
851 | 854 | ||
852 | const actorId = options.actorId || (await getServerActor()).id | 855 | // actorId === null has a meaning, so just check undefined |
856 | const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id | ||
853 | 857 | ||
854 | const scopes = { | 858 | const scopes = { |
855 | method: [ | 859 | method: [ |
@@ -926,7 +930,8 @@ export class VideoModel extends Model<VideoModel> { | |||
926 | id: { | 930 | id: { |
927 | [ Sequelize.Op.in ]: Sequelize.literal( | 931 | [ Sequelize.Op.in ]: Sequelize.literal( |
928 | '(' + | 932 | '(' + |
929 | 'SELECT "video"."id" FROM "video" WHERE ' + | 933 | 'SELECT "video"."id" FROM "video" ' + |
934 | 'WHERE ' + | ||
930 | 'lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' + | 935 | 'lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' + |
931 | 'lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' + | 936 | 'lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' + |
932 | 'UNION ALL ' + | 937 | 'UNION ALL ' + |
diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts index 512cb32fd..a287c5bdf 100644 --- a/server/tests/api/search/search-activitypub-video-channels.ts +++ b/server/tests/api/search/search-activitypub-video-channels.ts | |||
@@ -8,11 +8,11 @@ import { | |||
8 | deleteVideoChannel, | 8 | deleteVideoChannel, |
9 | flushAndRunMultipleServers, | 9 | flushAndRunMultipleServers, |
10 | flushTests, | 10 | flushTests, |
11 | getVideoChannelsList, | 11 | getVideoChannelsList, getVideoChannelVideos, |
12 | killallServers, | 12 | killallServers, |
13 | ServerInfo, | 13 | ServerInfo, |
14 | setAccessTokensToServers, | 14 | setAccessTokensToServers, |
15 | updateMyUser, | 15 | updateMyUser, updateVideo, |
16 | updateVideoChannel, | 16 | updateVideoChannel, |
17 | uploadVideo, | 17 | uploadVideo, |
18 | userLogin, | 18 | userLogin, |
@@ -27,6 +27,8 @@ const expect = chai.expect | |||
27 | describe('Test a ActivityPub video channels search', function () { | 27 | describe('Test a ActivityPub video channels search', function () { |
28 | let servers: ServerInfo[] | 28 | let servers: ServerInfo[] |
29 | let userServer2Token: string | 29 | let userServer2Token: string |
30 | let videoServer2UUID: string | ||
31 | let channelIdServer2: number | ||
30 | 32 | ||
31 | before(async function () { | 33 | before(async function () { |
32 | this.timeout(120000) | 34 | this.timeout(120000) |
@@ -56,10 +58,10 @@ describe('Test a ActivityPub video channels search', function () { | |||
56 | displayName: 'Channel 1 server 2' | 58 | displayName: 'Channel 1 server 2' |
57 | } | 59 | } |
58 | const resChannel = await addVideoChannel(servers[1].url, userServer2Token, channel) | 60 | const resChannel = await addVideoChannel(servers[1].url, userServer2Token, channel) |
59 | const channelId = resChannel.body.videoChannel.id | 61 | channelIdServer2 = resChannel.body.videoChannel.id |
60 | 62 | ||
61 | await uploadVideo(servers[1].url, userServer2Token, { name: 'video 1 server 2', channelId }) | 63 | const res = await uploadVideo(servers[1].url, userServer2Token, { name: 'video 1 server 2', channelId: channelIdServer2 }) |
62 | await uploadVideo(servers[1].url, userServer2Token, { name: 'video 2 server 2', channelId }) | 64 | videoServer2UUID = res.body.video.uuid |
63 | } | 65 | } |
64 | 66 | ||
65 | await waitJobs(servers) | 67 | await waitJobs(servers) |
@@ -129,6 +131,23 @@ describe('Test a ActivityPub video channels search', function () { | |||
129 | expect(res.body.data[2].name).to.equal('root_channel') | 131 | expect(res.body.data[2].name).to.equal('root_channel') |
130 | }) | 132 | }) |
131 | 133 | ||
134 | it('Should list video channel videos of server 2 without token', async function () { | ||
135 | this.timeout(30000) | ||
136 | |||
137 | await waitJobs(servers) | ||
138 | |||
139 | const res = await getVideoChannelVideos(servers[0].url, null, 'channel1_server2@localhost:9002', 0, 5) | ||
140 | expect(res.body.total).to.equal(0) | ||
141 | expect(res.body.data).to.have.lengthOf(0) | ||
142 | }) | ||
143 | |||
144 | it('Should list video channel videos of server 2 with token', async function () { | ||
145 | const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, 'channel1_server2@localhost:9002', 0, 5) | ||
146 | |||
147 | expect(res.body.total).to.equal(1) | ||
148 | expect(res.body.data[0].name).to.equal('video 1 server 2') | ||
149 | }) | ||
150 | |||
132 | it('Should update video channel of server 2, and refresh it on server 1', async function () { | 151 | it('Should update video channel of server 2, and refresh it on server 1', async function () { |
133 | this.timeout(60000) | 152 | this.timeout(60000) |
134 | 153 | ||
@@ -151,6 +170,29 @@ describe('Test a ActivityPub video channels search', function () { | |||
151 | // expect(videoChannel.ownerAccount.displayName).to.equal('user updated') | 170 | // expect(videoChannel.ownerAccount.displayName).to.equal('user updated') |
152 | }) | 171 | }) |
153 | 172 | ||
173 | it('Should update and add a video on server 2, and update it on server 1 after a search', async function () { | ||
174 | this.timeout(60000) | ||
175 | |||
176 | await updateVideo(servers[1].url, userServer2Token, videoServer2UUID, { name: 'video 1 updated' }) | ||
177 | await uploadVideo(servers[1].url, userServer2Token, { name: 'video 2 server 2', channelId: channelIdServer2 }) | ||
178 | |||
179 | await waitJobs(servers) | ||
180 | |||
181 | // Expire video channel | ||
182 | await wait(10000) | ||
183 | |||
184 | const search = 'http://localhost:9002/video-channels/channel1_server2' | ||
185 | await searchVideoChannel(servers[0].url, search, servers[0].accessToken) | ||
186 | |||
187 | await waitJobs(servers) | ||
188 | |||
189 | const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, 'channel1_server2@localhost:9002', 0, 5, '-createdAt') | ||
190 | |||
191 | expect(res.body.total).to.equal(2) | ||
192 | expect(res.body.data[0].name).to.equal('video 2 server 2') | ||
193 | expect(res.body.data[1].name).to.equal('video 1 updated') | ||
194 | }) | ||
195 | |||
154 | it('Should delete video channel of server 2, and delete it on server 1', async function () { | 196 | it('Should delete video channel of server 2, and delete it on server 1', async function () { |
155 | this.timeout(60000) | 197 | this.timeout(60000) |
156 | 198 | ||