aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/accounts.ts4
-rw-r--r--server/controllers/api/search.ts11
-rw-r--r--server/controllers/api/video-channel.ts4
-rw-r--r--server/helpers/express-utils.ts8
-rw-r--r--server/lib/activitypub/actor.ts31
-rw-r--r--server/models/video/video.ts63
-rw-r--r--server/tests/api/search/search-activitypub-video-channels.ts52
7 files changed, 118 insertions, 55 deletions
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 {
11import { accountsNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators' 11import { accountsNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators'
12import { AccountModel } from '../../models/account/account' 12import { AccountModel } from '../../models/account/account'
13import { VideoModel } from '../../models/video/video' 13import { VideoModel } from '../../models/video/video'
14import { buildNSFWFilter } from '../../helpers/express-utils' 14import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
15import { VideoChannelModel } from '../../models/video/video-channel' 15import { VideoChannelModel } from '../../models/video/video-channel'
16 16
17const accountsRouter = express.Router() 17const accountsRouter = express.Router()
@@ -73,8 +73,10 @@ async function listVideoAccountChannels (req: express.Request, res: express.Resp
73 73
74async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 74async 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { buildNSFWFilter } from '../../helpers/express-utils' 2import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
3import { getFormattedObjects, getServerActor } from '../../helpers/utils' 3import { getFormattedObjects, getServerActor } from '../../helpers/utils'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
5import { 5import {
@@ -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
156function 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
19import { sendUpdateActor } from '../../lib/activitypub/send' 19import { sendUpdateActor } from '../../lib/activitypub/send'
20import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 20import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
21import { createVideoChannel } from '../../lib/video-channel' 21import { createVideoChannel } from '../../lib/video-channel'
22import { buildNSFWFilter, createReqFiles } from '../../helpers/express-utils' 22import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
23import { setAsyncActorKeys } from '../../lib/activitypub' 23import { setAsyncActorKeys } from '../../lib/activitypub'
24import { AccountModel } from '../../models/account/account' 24import { AccountModel } from '../../models/account/account'
25import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' 25import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
@@ -210,8 +210,10 @@ async function getVideoChannel (req: express.Request, res: express.Response, nex
210 210
211async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 211async 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
98function 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
100export { 107export {
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
39async function getOrCreateActorAndServerAndModel (activityActor: string | ActivityPubActor, recurseIfNeeded = true) { 39async 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
82function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) { 93function 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
362async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> { 373async 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
27describe('Test a ActivityPub video channels search', function () { 27describe('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