aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-08-23 17:58:39 +0200
committerChocobozzz <me@florianbigard.com>2018-08-27 09:41:54 +0200
commitf37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9 (patch)
tree2050443febcdb2a3eec68b7bbf9687e26dcb24dc /server
parent240085d0056fd97ac3c7fa8fa4ce9bc32afc4d6e (diff)
downloadPeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.tar.gz
PeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.tar.zst
PeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.zip
Add ability to search video channels
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/search.ts96
-rw-r--r--server/controllers/api/users/me.ts41
-rw-r--r--server/controllers/api/video-channel.ts5
-rw-r--r--server/helpers/custom-validators/activitypub/actor.ts7
-rw-r--r--server/initializers/constants.ts3
-rw-r--r--server/lib/activitypub/process/process-update.ts4
-rw-r--r--server/lib/activitypub/videos.ts8
-rw-r--r--server/middlewares/validators/follows.ts2
-rw-r--r--server/middlewares/validators/search.ts19
-rw-r--r--server/middlewares/validators/sort.ts5
-rw-r--r--server/middlewares/validators/user-subscriptions.ts24
-rw-r--r--server/models/account/account.ts14
-rw-r--r--server/models/activitypub/actor-follow.ts103
-rw-r--r--server/models/activitypub/actor.ts10
-rw-r--r--server/models/video/video-channel.ts163
-rw-r--r--server/tests/api/check-params/user-subscriptions.ts40
-rw-r--r--server/tests/api/users/user-subscriptions.ts19
-rw-r--r--server/tests/utils/users/user-subscriptions.ts13
18 files changed, 490 insertions, 86 deletions
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index f408e7932..87aa5d76f 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -1,22 +1,26 @@
1import * as express from 'express' 1import * as express from 'express'
2import { buildNSFWFilter } from '../../helpers/express-utils' 2import { buildNSFWFilter } from '../../helpers/express-utils'
3import { getFormattedObjects } from '../../helpers/utils' 3import { getFormattedObjects, getServerActor } from '../../helpers/utils'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
5import { 5import {
6 asyncMiddleware, 6 asyncMiddleware,
7 commonVideosFiltersValidator, 7 commonVideosFiltersValidator,
8 optionalAuthenticate, 8 optionalAuthenticate,
9 paginationValidator, 9 paginationValidator,
10 searchValidator,
11 setDefaultPagination, 10 setDefaultPagination,
12 setDefaultSearchSort, 11 setDefaultSearchSort,
13 videosSearchSortValidator 12 videoChannelsSearchSortValidator,
13 videoChannelsSearchValidator,
14 videosSearchSortValidator,
15 videosSearchValidator
14} from '../../middlewares' 16} from '../../middlewares'
15import { VideosSearchQuery } from '../../../shared/models/search' 17import { VideoChannelsSearchQuery, VideosSearchQuery } from '../../../shared/models/search'
16import { getOrCreateVideoAndAccountAndChannel } from '../../lib/activitypub' 18import { getOrCreateActorAndServerAndModel, getOrCreateVideoAndAccountAndChannel } from '../../lib/activitypub'
17import { logger } from '../../helpers/logger' 19import { logger } from '../../helpers/logger'
18import { User } from '../../../shared/models/users' 20import { User } from '../../../shared/models/users'
19import { CONFIG } from '../../initializers/constants' 21import { CONFIG } from '../../initializers/constants'
22import { VideoChannelModel } from '../../models/video/video-channel'
23import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
20 24
21const searchRouter = express.Router() 25const searchRouter = express.Router()
22 26
@@ -27,21 +31,80 @@ searchRouter.get('/videos',
27 setDefaultSearchSort, 31 setDefaultSearchSort,
28 optionalAuthenticate, 32 optionalAuthenticate,
29 commonVideosFiltersValidator, 33 commonVideosFiltersValidator,
30 searchValidator, 34 videosSearchValidator,
31 asyncMiddleware(searchVideos) 35 asyncMiddleware(searchVideos)
32) 36)
33 37
38searchRouter.get('/video-channels',
39 paginationValidator,
40 setDefaultPagination,
41 videoChannelsSearchSortValidator,
42 setDefaultSearchSort,
43 optionalAuthenticate,
44 commonVideosFiltersValidator,
45 videoChannelsSearchValidator,
46 asyncMiddleware(searchVideoChannels)
47)
48
34// --------------------------------------------------------------------------- 49// ---------------------------------------------------------------------------
35 50
36export { searchRouter } 51export { searchRouter }
37 52
38// --------------------------------------------------------------------------- 53// ---------------------------------------------------------------------------
39 54
55function searchVideoChannels (req: express.Request, res: express.Response) {
56 const query: VideoChannelsSearchQuery = req.query
57 const search = query.search
58
59 const isURISearch = search.startsWith('http://') || search.startsWith('https://')
60
61 const parts = search.split('@')
62 const isHandleSearch = parts.length === 2 && parts.every(p => p.indexOf(' ') === -1)
63
64 if (isURISearch || isHandleSearch) return searchVideoChannelURI(search, isHandleSearch, res)
65
66 return searchVideoChannelsDB(query, res)
67}
68
69async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) {
70 const serverActor = await getServerActor()
71
72 const options = {
73 actorId: serverActor.id,
74 search: query.search,
75 start: query.start,
76 count: query.count,
77 sort: query.sort
78 }
79 const resultList = await VideoChannelModel.searchForApi(options)
80
81 return res.json(getFormattedObjects(resultList.data, resultList.total))
82}
83
84async function searchVideoChannelURI (search: string, isHandleSearch: boolean, res: express.Response) {
85 let videoChannel: VideoChannelModel
86
87 if (isUserAbleToSearchRemoteURI(res)) {
88 let uri = search
89 if (isHandleSearch) uri = await loadActorUrlOrGetFromWebfinger(search)
90
91 const actor = await getOrCreateActorAndServerAndModel(uri)
92 videoChannel = actor.VideoChannel
93 } else {
94 videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(search)
95 }
96
97 return res.json({
98 total: videoChannel ? 1 : 0,
99 data: videoChannel ? [ videoChannel.toFormattedJSON() ] : []
100 })
101}
102
40function searchVideos (req: express.Request, res: express.Response) { 103function searchVideos (req: express.Request, res: express.Response) {
41 const query: VideosSearchQuery = req.query 104 const query: VideosSearchQuery = req.query
42 const search = query.search 105 const search = query.search
43 if (search && (search.startsWith('http://') || search.startsWith('https://'))) { 106 if (search && (search.startsWith('http://') || search.startsWith('https://'))) {
44 return searchVideoUrl(search, res) 107 return searchVideoURI(search, res)
45 } 108 }
46 109
47 return searchVideosDB(query, res) 110 return searchVideosDB(query, res)
@@ -57,15 +120,11 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response)
57 return res.json(getFormattedObjects(resultList.data, resultList.total)) 120 return res.json(getFormattedObjects(resultList.data, resultList.total))
58} 121}
59 122
60async function searchVideoUrl (url: string, res: express.Response) { 123async function searchVideoURI (url: string, res: express.Response) {
61 let video: VideoModel 124 let video: VideoModel
62 const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined
63 125
64 // Check if we can fetch a remote video with the URL 126 // Check if we can fetch a remote video with the URL
65 if ( 127 if (isUserAbleToSearchRemoteURI(res)) {
66 CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
67 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
68 ) {
69 try { 128 try {
70 const syncParam = { 129 const syncParam = {
71 likes: false, 130 likes: false,
@@ -76,8 +135,8 @@ async function searchVideoUrl (url: string, res: express.Response) {
76 refreshVideo: false 135 refreshVideo: false
77 } 136 }
78 137
79 const res = await getOrCreateVideoAndAccountAndChannel(url, syncParam) 138 const result = await getOrCreateVideoAndAccountAndChannel(url, syncParam)
80 video = res ? res.video : undefined 139 video = result ? result.video : undefined
81 } catch (err) { 140 } catch (err) {
82 logger.info('Cannot search remote video %s.', url) 141 logger.info('Cannot search remote video %s.', url)
83 } 142 }
@@ -90,3 +149,10 @@ async function searchVideoUrl (url: string, res: express.Response) {
90 data: video ? [ video.toFormattedJSON() ] : [] 149 data: video ? [ video.toFormattedJSON() ] : []
91 }) 150 })
92} 151}
152
153function isUserAbleToSearchRemoteURI (res: express.Response) {
154 const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined
155
156 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
157 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
158}
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 2300f5dbe..000c706b5 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -20,7 +20,8 @@ import {
20 deleteMeValidator, 20 deleteMeValidator,
21 userSubscriptionsSortValidator, 21 userSubscriptionsSortValidator,
22 videoImportsSortValidator, 22 videoImportsSortValidator,
23 videosSortValidator 23 videosSortValidator,
24 areSubscriptionsExistValidator
24} from '../../../middlewares/validators' 25} from '../../../middlewares/validators'
25import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 26import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
26import { UserModel } from '../../../models/account/user' 27import { UserModel } from '../../../models/account/user'
@@ -99,7 +100,6 @@ meRouter.post('/me/avatar/pick',
99 100
100meRouter.get('/me/subscriptions/videos', 101meRouter.get('/me/subscriptions/videos',
101 authenticate, 102 authenticate,
102 authenticate,
103 paginationValidator, 103 paginationValidator,
104 videosSortValidator, 104 videosSortValidator,
105 setDefaultSort, 105 setDefaultSort,
@@ -108,6 +108,12 @@ meRouter.get('/me/subscriptions/videos',
108 asyncMiddleware(getUserSubscriptionVideos) 108 asyncMiddleware(getUserSubscriptionVideos)
109) 109)
110 110
111meRouter.get('/me/subscriptions/exist',
112 authenticate,
113 areSubscriptionsExistValidator,
114 asyncMiddleware(areSubscriptionsExist)
115)
116
111meRouter.get('/me/subscriptions', 117meRouter.get('/me/subscriptions',
112 authenticate, 118 authenticate,
113 paginationValidator, 119 paginationValidator,
@@ -143,6 +149,37 @@ export {
143 149
144// --------------------------------------------------------------------------- 150// ---------------------------------------------------------------------------
145 151
152async function areSubscriptionsExist (req: express.Request, res: express.Response) {
153 const uris = req.query.uris as string[]
154 const user = res.locals.oauth.token.User as UserModel
155
156 const handles = uris.map(u => {
157 let [ name, host ] = u.split('@')
158 if (host === CONFIG.WEBSERVER.HOST) host = null
159
160 return { name, host, uri: u }
161 })
162
163 const results = await ActorFollowModel.listSubscribedIn(user.Account.Actor.id, handles)
164
165 const existObject: { [id: string ]: boolean } = {}
166 for (const handle of handles) {
167 const obj = results.find(r => {
168 const server = r.ActorFollowing.Server
169
170 return r.ActorFollowing.preferredUsername === handle.name &&
171 (
172 (!server && !handle.host) ||
173 (server.host === handle.host)
174 )
175 })
176
177 existObject[handle.uri] = obj !== undefined
178 }
179
180 return res.json(existObject)
181}
182
146async function addUserSubscription (req: express.Request, res: express.Response) { 183async function addUserSubscription (req: express.Request, res: express.Response) {
147 const user = res.locals.oauth.token.User as UserModel 184 const user = res.locals.oauth.token.User as UserModel
148 const [ name, host ] = req.body.uri.split('@') 185 const [ name, host ] = req.body.uri.split('@')
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 3f51f03f4..bd08d7a08 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects } from '../../helpers/utils' 2import { getFormattedObjects, getServerActor } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, 4 asyncMiddleware,
5 asyncRetryTransactionMiddleware, 5 asyncRetryTransactionMiddleware,
@@ -95,7 +95,8 @@ export {
95// --------------------------------------------------------------------------- 95// ---------------------------------------------------------------------------
96 96
97async function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) { 97async function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) {
98 const resultList = await VideoChannelModel.listForApi(req.query.start, req.query.count, req.query.sort) 98 const serverActor = await getServerActor()
99 const resultList = await VideoChannelModel.listForApi(serverActor.id, req.query.start, req.query.count, req.query.sort)
99 100
100 return res.json(getFormattedObjects(resultList.data, resultList.total)) 101 return res.json(getFormattedObjects(resultList.data, resultList.total))
101} 102}
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts
index c3a62c12d..6958b2b00 100644
--- a/server/helpers/custom-validators/activitypub/actor.ts
+++ b/server/helpers/custom-validators/activitypub/actor.ts
@@ -1,6 +1,6 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { CONSTRAINTS_FIELDS } from '../../../initializers' 2import { CONSTRAINTS_FIELDS } from '../../../initializers'
3import { exists } from '../misc' 3import { exists, isArray } from '../misc'
4import { truncate } from 'lodash' 4import { truncate } from 'lodash'
5import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 5import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
6import { isHostValid } from '../servers' 6import { isHostValid } from '../servers'
@@ -119,10 +119,15 @@ function isValidActorHandle (handle: string) {
119 return isHostValid(parts[1]) 119 return isHostValid(parts[1])
120} 120}
121 121
122function areValidActorHandles (handles: string[]) {
123 return isArray(handles) && handles.every(h => isValidActorHandle(h))
124}
125
122// --------------------------------------------------------------------------- 126// ---------------------------------------------------------------------------
123 127
124export { 128export {
125 normalizeActor, 129 normalizeActor,
130 areValidActorHandles,
126 isActorEndpointsObjectValid, 131 isActorEndpointsObjectValid,
127 isActorPublicKeyObjectValid, 132 isActorPublicKeyObjectValid,
128 isActorTypeValid, 133 isActorTypeValid,
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 46b63c5e9..9beb9b7c2 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -43,7 +43,8 @@ const SORTABLE_COLUMNS = {
43 FOLLOWERS: [ 'createdAt' ], 43 FOLLOWERS: [ 'createdAt' ],
44 FOLLOWING: [ 'createdAt' ], 44 FOLLOWING: [ 'createdAt' ],
45 45
46 VIDEOS_SEARCH: [ 'match', 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ] 46 VIDEOS_SEARCH: [ 'match', 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ],
47 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName' ]
47} 48}
48 49
49const OAUTH_LIFETIME = { 50const OAUTH_LIFETIME = {
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 07a5ff92f..d2ad738a2 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -7,7 +7,7 @@ import { AccountModel } from '../../../models/account/account'
7import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
8import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
9import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' 9import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannel, updateVideoFromAP } from '../videos' 10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' 11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
12 12
13async function processUpdateActivity (activity: ActivityUpdate) { 13async function processUpdateActivity (activity: ActivityUpdate) {
@@ -40,7 +40,7 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate)
40 } 40 }
41 41
42 const { video } = await getOrCreateVideoAndAccountAndChannel(videoObject.id) 42 const { video } = await getOrCreateVideoAndAccountAndChannel(videoObject.id)
43 const channelActor = await getOrCreateVideoChannel(videoObject) 43 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
44 44
45 return updateVideoFromAP(video, videoObject, actor, channelActor, activity.to) 45 return updateVideoFromAP(video, videoObject, actor, channelActor, activity.to)
46} 46}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 388c31fe5..6c2095897 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -174,7 +174,7 @@ function videoFileActivityUrlToDBAttributes (videoCreated: VideoModel, videoObje
174 return attributes 174 return attributes
175} 175}
176 176
177function getOrCreateVideoChannel (videoObject: VideoTorrentObject) { 177function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
178 const channel = videoObject.attributedTo.find(a => a.type === 'Group') 178 const channel = videoObject.attributedTo.find(a => a.type === 'Group')
179 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) 179 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
180 180
@@ -251,7 +251,7 @@ async function getOrCreateVideoAndAccountAndChannel (
251 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) 251 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
252 if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl) 252 if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
253 253
254 const channelActor = await getOrCreateVideoChannel(fetchedVideo) 254 const channelActor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo)
255 const video = await retryTransactionWrapper(createVideo, fetchedVideo, channelActor, syncParam.thumbnail) 255 const video = await retryTransactionWrapper(createVideo, fetchedVideo, channelActor, syncParam.thumbnail)
256 256
257 // Process outside the transaction because we could fetch remote data 257 // Process outside the transaction because we could fetch remote data
@@ -329,7 +329,7 @@ async function refreshVideoIfNeeded (video: VideoModel): Promise<VideoModel> {
329 return video 329 return video
330 } 330 }
331 331
332 const channelActor = await getOrCreateVideoChannel(videoObject) 332 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
333 const account = await AccountModel.load(channelActor.VideoChannel.accountId) 333 const account = await AccountModel.load(channelActor.VideoChannel.accountId)
334 return updateVideoFromAP(video, videoObject, account.Actor, channelActor) 334 return updateVideoFromAP(video, videoObject, account.Actor, channelActor)
335 335
@@ -440,7 +440,7 @@ export {
440 videoActivityObjectToDBAttributes, 440 videoActivityObjectToDBAttributes,
441 videoFileActivityUrlToDBAttributes, 441 videoFileActivityUrlToDBAttributes,
442 createVideo, 442 createVideo,
443 getOrCreateVideoChannel, 443 getOrCreateVideoChannelFromVideoObject,
444 addVideoShares, 444 addVideoShares,
445 createRates 445 createRates
446} 446}
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
index faefc1179..73fa28be9 100644
--- a/server/middlewares/validators/follows.ts
+++ b/server/middlewares/validators/follows.ts
@@ -38,7 +38,7 @@ const removeFollowingValidator = [
38 if (areValidationErrors(req, res)) return 38 if (areValidationErrors(req, res)) return
39 39
40 const serverActor = await getServerActor() 40 const serverActor = await getServerActor()
41 const follow = await ActorFollowModel.loadByActorAndTargetNameAndHost(serverActor.id, SERVER_ACTOR_NAME, req.params.host) 41 const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host)
42 42
43 if (!follow) { 43 if (!follow) {
44 return res 44 return res
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts
index e516c4c41..8baf643a5 100644
--- a/server/middlewares/validators/search.ts
+++ b/server/middlewares/validators/search.ts
@@ -5,7 +5,7 @@ import { query } from 'express-validator/check'
5import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search' 5import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search'
6import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc' 6import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc'
7 7
8const searchValidator = [ 8const videosSearchValidator = [
9 query('search').optional().not().isEmpty().withMessage('Should have a valid search'), 9 query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
10 10
11 query('startDate').optional().custom(isDateValid).withMessage('Should have a valid start date'), 11 query('startDate').optional().custom(isDateValid).withMessage('Should have a valid start date'),
@@ -15,7 +15,19 @@ const searchValidator = [
15 query('durationMax').optional().isInt().withMessage('Should have a valid max duration'), 15 query('durationMax').optional().isInt().withMessage('Should have a valid max duration'),
16 16
17 (req: express.Request, res: express.Response, next: express.NextFunction) => { 17 (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 logger.debug('Checking search query', { parameters: req.query }) 18 logger.debug('Checking videos search query', { parameters: req.query })
19
20 if (areValidationErrors(req, res)) return
21
22 return next()
23 }
24]
25
26const videoChannelsSearchValidator = [
27 query('search').not().isEmpty().withMessage('Should have a valid search'),
28
29 (req: express.Request, res: express.Response, next: express.NextFunction) => {
30 logger.debug('Checking video channels search query', { parameters: req.query })
19 31
20 if (areValidationErrors(req, res)) return 32 if (areValidationErrors(req, res)) return
21 33
@@ -61,5 +73,6 @@ const commonVideosFiltersValidator = [
61 73
62export { 74export {
63 commonVideosFiltersValidator, 75 commonVideosFiltersValidator,
64 searchValidator 76 videoChannelsSearchValidator,
77 videosSearchValidator
65} 78}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index b30e97e61..08dcc2680 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -8,6 +8,7 @@ const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
8const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) 8const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
9const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) 9const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
10const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) 10const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH)
11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
11const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS) 12const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS)
12const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) 13const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
13const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) 14const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
@@ -23,6 +24,7 @@ const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
23const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) 24const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
24const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) 25const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
25const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) 26const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
27const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS)
26const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) 28const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS)
27const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) 29const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
28const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) 30const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
@@ -45,5 +47,6 @@ export {
45 followingSortValidator, 47 followingSortValidator,
46 jobsSortValidator, 48 jobsSortValidator,
47 videoCommentThreadsSortValidator, 49 videoCommentThreadsSortValidator,
48 userSubscriptionsSortValidator 50 userSubscriptionsSortValidator,
51 videoChannelsSearchSortValidator
49} 52}
diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts
index d8c26c742..c5f8d9d4c 100644
--- a/server/middlewares/validators/user-subscriptions.ts
+++ b/server/middlewares/validators/user-subscriptions.ts
@@ -1,12 +1,13 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param } from 'express-validator/check' 3import { body, param, query } from 'express-validator/check'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6import { ActorFollowModel } from '../../models/activitypub/actor-follow' 6import { ActorFollowModel } from '../../models/activitypub/actor-follow'
7import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' 7import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
8import { UserModel } from '../../models/account/user' 8import { UserModel } from '../../models/account/user'
9import { CONFIG } from '../../initializers' 9import { CONFIG } from '../../initializers'
10import { toArray } from '../../helpers/custom-validators/misc'
10 11
11const userSubscriptionAddValidator = [ 12const userSubscriptionAddValidator = [
12 body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'), 13 body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'),
@@ -20,6 +21,20 @@ const userSubscriptionAddValidator = [
20 } 21 }
21] 22]
22 23
24const areSubscriptionsExistValidator = [
25 query('uris')
26 .customSanitizer(toArray)
27 .custom(areValidActorHandles).withMessage('Should have a valid uri array'),
28
29 (req: express.Request, res: express.Response, next: express.NextFunction) => {
30 logger.debug('Checking areSubscriptionsExistValidator parameters', { parameters: req.query })
31
32 if (areValidationErrors(req, res)) return
33
34 return next()
35 }
36]
37
23const userSubscriptionGetValidator = [ 38const userSubscriptionGetValidator = [
24 param('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to unfollow'), 39 param('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to unfollow'),
25 40
@@ -32,7 +47,7 @@ const userSubscriptionGetValidator = [
32 if (host === CONFIG.WEBSERVER.HOST) host = null 47 if (host === CONFIG.WEBSERVER.HOST) host = null
33 48
34 const user: UserModel = res.locals.oauth.token.User 49 const user: UserModel = res.locals.oauth.token.User
35 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHost(user.Account.Actor.id, name, host) 50 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host)
36 51
37 if (!subscription || !subscription.ActorFollowing.VideoChannel) { 52 if (!subscription || !subscription.ActorFollowing.VideoChannel) {
38 return res 53 return res
@@ -51,8 +66,7 @@ const userSubscriptionGetValidator = [
51// --------------------------------------------------------------------------- 66// ---------------------------------------------------------------------------
52 67
53export { 68export {
69 areSubscriptionsExistValidator,
54 userSubscriptionAddValidator, 70 userSubscriptionAddValidator,
55 userSubscriptionGetValidator 71 userSubscriptionGetValidator
56} 72}
57
58// ---------------------------------------------------------------------------
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 07539a04e..6bbfc6f4e 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -29,18 +29,8 @@ import { UserModel } from './user'
29@DefaultScope({ 29@DefaultScope({
30 include: [ 30 include: [
31 { 31 {
32 model: () => ActorModel, 32 model: () => ActorModel, // Default scope includes avatar and server
33 required: true, 33 required: true
34 include: [
35 {
36 model: () => ServerModel,
37 required: false
38 },
39 {
40 model: () => AvatarModel,
41 required: false
42 }
43 ]
44 } 34 }
45 ] 35 ]
46}) 36})
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index b2d7ace66..81fcf7001 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -26,7 +26,7 @@ import { ACTOR_FOLLOW_SCORE } from '../../initializers'
26import { FOLLOW_STATES } from '../../initializers/constants' 26import { FOLLOW_STATES } from '../../initializers/constants'
27import { ServerModel } from '../server/server' 27import { ServerModel } from '../server/server'
28import { getSort } from '../utils' 28import { getSort } from '../utils'
29import { ActorModel } from './actor' 29import { ActorModel, unusedActorAttributesForAPI } from './actor'
30import { VideoChannelModel } from '../video/video-channel' 30import { VideoChannelModel } from '../video/video-channel'
31import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions' 31import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions'
32import { AccountModel } from '../account/account' 32import { AccountModel } from '../account/account'
@@ -167,8 +167,11 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
167 return ActorFollowModel.findOne(query) 167 return ActorFollowModel.findOne(query)
168 } 168 }
169 169
170 static loadByActorAndTargetNameAndHost (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) { 170 static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) {
171 const actorFollowingPartInclude: IIncludeOptions = { 171 const actorFollowingPartInclude: IIncludeOptions = {
172 attributes: {
173 exclude: unusedActorAttributesForAPI
174 },
172 model: ActorModel, 175 model: ActorModel,
173 required: true, 176 required: true,
174 as: 'ActorFollowing', 177 as: 'ActorFollowing',
@@ -177,7 +180,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
177 }, 180 },
178 include: [ 181 include: [
179 { 182 {
180 model: VideoChannelModel, 183 model: VideoChannelModel.unscoped(),
181 required: false 184 required: false
182 } 185 }
183 ] 186 ]
@@ -200,17 +203,79 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
200 actorId 203 actorId
201 }, 204 },
202 include: [ 205 include: [
203 {
204 model: ActorModel,
205 required: true,
206 as: 'ActorFollower'
207 },
208 actorFollowingPartInclude 206 actorFollowingPartInclude
209 ], 207 ],
210 transaction: t 208 transaction: t
211 } 209 }
212 210
213 return ActorFollowModel.findOne(query) 211 return ActorFollowModel.findOne(query)
212 .then(result => {
213 if (result && result.ActorFollowing.VideoChannel) {
214 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
215 }
216
217 return result
218 })
219 }
220
221 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]) {
222 const whereTab = targets
223 .map(t => {
224 if (t.host) {
225 return {
226 [ Sequelize.Op.and ]: [
227 {
228 '$preferredUsername$': t.name
229 },
230 {
231 '$host$': t.host
232 }
233 ]
234 }
235 }
236
237 return {
238 [ Sequelize.Op.and ]: [
239 {
240 '$preferredUsername$': t.name
241 },
242 {
243 '$serverId$': null
244 }
245 ]
246 }
247 })
248
249 const query = {
250 attributes: [],
251 where: {
252 [ Sequelize.Op.and ]: [
253 {
254 [ Sequelize.Op.or ]: whereTab
255 },
256 {
257 actorId
258 }
259 ]
260 },
261 include: [
262 {
263 attributes: [ 'preferredUsername' ],
264 model: ActorModel.unscoped(),
265 required: true,
266 as: 'ActorFollowing',
267 include: [
268 {
269 attributes: [ 'host' ],
270 model: ServerModel.unscoped(),
271 required: false
272 }
273 ]
274 }
275 ]
276 }
277
278 return ActorFollowModel.findAll(query)
214 } 279 }
215 280
216 static listFollowingForApi (id: number, start: number, count: number, sort: string) { 281 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
@@ -248,6 +313,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
248 313
249 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { 314 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
250 const query = { 315 const query = {
316 attributes: [],
251 distinct: true, 317 distinct: true,
252 offset: start, 318 offset: start,
253 limit: count, 319 limit: count,
@@ -257,6 +323,9 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
257 }, 323 },
258 include: [ 324 include: [
259 { 325 {
326 attributes: {
327 exclude: unusedActorAttributesForAPI
328 },
260 model: ActorModel, 329 model: ActorModel,
261 as: 'ActorFollowing', 330 as: 'ActorFollowing',
262 required: true, 331 required: true,
@@ -266,8 +335,24 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
266 required: true, 335 required: true,
267 include: [ 336 include: [
268 { 337 {
269 model: AccountModel, 338 attributes: {
339 exclude: unusedActorAttributesForAPI
340 },
341 model: ActorModel,
270 required: true 342 required: true
343 },
344 {
345 model: AccountModel,
346 required: true,
347 include: [
348 {
349 attributes: {
350 exclude: unusedActorAttributesForAPI
351 },
352 model: ActorModel,
353 required: true
354 }
355 ]
271 } 356 }
272 ] 357 ]
273 } 358 }
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 2abf40713..ec0b4b2d9 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -42,6 +42,16 @@ enum ScopeNames {
42 FULL = 'FULL' 42 FULL = 'FULL'
43} 43}
44 44
45export const unusedActorAttributesForAPI = [
46 'publicKey',
47 'privateKey',
48 'inboxUrl',
49 'outboxUrl',
50 'sharedInboxUrl',
51 'followersUrl',
52 'followingUrl'
53]
54
45@DefaultScope({ 55@DefaultScope({
46 include: [ 56 include: [
47 { 57 {
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 9f80e0b8d..7d717fc68 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -12,6 +12,7 @@ import {
12 Is, 12 Is,
13 Model, 13 Model,
14 Scopes, 14 Scopes,
15 Sequelize,
15 Table, 16 Table,
16 UpdatedAt 17 UpdatedAt
17} from 'sequelize-typescript' 18} from 'sequelize-typescript'
@@ -24,19 +25,36 @@ import {
24} from '../../helpers/custom-validators/video-channels' 25} from '../../helpers/custom-validators/video-channels'
25import { sendDeleteActor } from '../../lib/activitypub/send' 26import { sendDeleteActor } from '../../lib/activitypub/send'
26import { AccountModel } from '../account/account' 27import { AccountModel } from '../account/account'
27import { ActorModel } from '../activitypub/actor' 28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
28import { getSort, throwIfNotValid } from '../utils' 29import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
29import { VideoModel } from './video' 30import { VideoModel } from './video'
30import { CONSTRAINTS_FIELDS } from '../../initializers' 31import { CONSTRAINTS_FIELDS } from '../../initializers'
31import { AvatarModel } from '../avatar/avatar'
32import { ServerModel } from '../server/server' 32import { ServerModel } from '../server/server'
33import { DefineIndexesOptions } from 'sequelize'
34
35// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
36const indexes: DefineIndexesOptions[] = [
37 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
38
39 {
40 fields: [ 'accountId' ]
41 },
42 {
43 fields: [ 'actorId' ]
44 }
45]
33 46
34enum ScopeNames { 47enum ScopeNames {
48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
35 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
36 WITH_ACTOR = 'WITH_ACTOR', 50 WITH_ACTOR = 'WITH_ACTOR',
37 WITH_VIDEOS = 'WITH_VIDEOS' 51 WITH_VIDEOS = 'WITH_VIDEOS'
38} 52}
39 53
54type AvailableForListOptions = {
55 actorId: number
56}
57
40@DefaultScope({ 58@DefaultScope({
41 include: [ 59 include: [
42 { 60 {
@@ -46,23 +64,57 @@ enum ScopeNames {
46 ] 64 ]
47}) 65})
48@Scopes({ 66@Scopes({
49 [ScopeNames.WITH_ACCOUNT]: { 67 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
50 include: [ 68 const actorIdNumber = parseInt(options.actorId + '', 10)
51 { 69
52 model: () => AccountModel.unscoped(), 70 // Only list local channels OR channels that are on an instance followed by actorId
53 required: true, 71 const inQueryInstanceFollow = '(' +
54 include: [ 72 'SELECT "actor"."serverId" FROM "actor" ' +
55 { 73 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = actor.id ' +
56 model: () => ActorModel.unscoped(), 74 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
57 required: true, 75 ')'
58 include: [ 76
77 return {
78 include: [
79 {
80 attributes: {
81 exclude: unusedActorAttributesForAPI
82 },
83 model: ActorModel,
84 where: {
85 [Sequelize.Op.or]: [
86 {
87 serverId: null
88 },
59 { 89 {
60 model: () => AvatarModel.unscoped(), 90 serverId: {
61 required: false 91 [ Sequelize.Op.in ]: Sequelize.literal(inQueryInstanceFollow)
92 }
62 } 93 }
63 ] 94 ]
64 } 95 }
65 ] 96 },
97 {
98 model: AccountModel,
99 required: true,
100 include: [
101 {
102 attributes: {
103 exclude: unusedActorAttributesForAPI
104 },
105 model: ActorModel, // Default scope includes avatar and server
106 required: true
107 }
108 ]
109 }
110 ]
111 }
112 },
113 [ScopeNames.WITH_ACCOUNT]: {
114 include: [
115 {
116 model: () => AccountModel,
117 required: true
66 } 118 }
67 ] 119 ]
68 }, 120 },
@@ -79,14 +131,7 @@ enum ScopeNames {
79}) 131})
80@Table({ 132@Table({
81 tableName: 'videoChannel', 133 tableName: 'videoChannel',
82 indexes: [ 134 indexes
83 {
84 fields: [ 'accountId' ]
85 },
86 {
87 fields: [ 'actorId' ]
88 }
89 ]
90}) 135})
91export class VideoChannelModel extends Model<VideoChannelModel> { 136export class VideoChannelModel extends Model<VideoChannelModel> {
92 137
@@ -170,15 +215,61 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
170 return VideoChannelModel.count(query) 215 return VideoChannelModel.count(query)
171 } 216 }
172 217
173 static listForApi (start: number, count: number, sort: string) { 218 static listForApi (actorId: number, start: number, count: number, sort: string) {
174 const query = { 219 const query = {
175 offset: start, 220 offset: start,
176 limit: count, 221 limit: count,
177 order: getSort(sort) 222 order: getSort(sort)
178 } 223 }
179 224
225 const scopes = {
226 method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId } as AvailableForListOptions ]
227 }
180 return VideoChannelModel 228 return VideoChannelModel
181 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 229 .scope(scopes)
230 .findAndCountAll(query)
231 .then(({ rows, count }) => {
232 return { total: count, data: rows }
233 })
234 }
235
236 static searchForApi (options: {
237 actorId: number
238 search: string
239 start: number
240 count: number
241 sort: string
242 }) {
243 const attributesInclude = []
244 const escapedSearch = VideoModel.sequelize.escape(options.search)
245 const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%')
246 attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search))
247
248 const query = {
249 attributes: {
250 include: attributesInclude
251 },
252 offset: options.start,
253 limit: options.count,
254 order: getSort(options.sort),
255 where: {
256 id: {
257 [ Sequelize.Op.in ]: Sequelize.literal(
258 '(' +
259 'SELECT id FROM "videoChannel" WHERE ' +
260 'lower(immutable_unaccent("name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
261 'lower(immutable_unaccent("name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
262 ')'
263 )
264 }
265 }
266 }
267
268 const scopes = {
269 method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId: options.actorId } as AvailableForListOptions ]
270 }
271 return VideoChannelModel
272 .scope(scopes)
182 .findAndCountAll(query) 273 .findAndCountAll(query)
183 .then(({ rows, count }) => { 274 .then(({ rows, count }) => {
184 return { total: count, data: rows } 275 return { total: count, data: rows }
@@ -239,7 +330,25 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
239 } 330 }
240 331
241 return VideoChannelModel 332 return VideoChannelModel
242 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 333 .scope([ ScopeNames.WITH_ACCOUNT ])
334 .findOne(query)
335 }
336
337 static loadByUrlAndPopulateAccount (url: string) {
338 const query = {
339 include: [
340 {
341 model: ActorModel,
342 required: true,
343 where: {
344 url
345 }
346 }
347 ]
348 }
349
350 return VideoChannelModel
351 .scope([ ScopeNames.WITH_ACCOUNT ])
243 .findOne(query) 352 .findOne(query)
244 } 353 }
245 354
diff --git a/server/tests/api/check-params/user-subscriptions.ts b/server/tests/api/check-params/user-subscriptions.ts
index 6a6dd9a6f..9fba99ac8 100644
--- a/server/tests/api/check-params/user-subscriptions.ts
+++ b/server/tests/api/check-params/user-subscriptions.ts
@@ -202,6 +202,46 @@ describe('Test user subscriptions API validators', function () {
202 }) 202 })
203 }) 203 })
204 204
205 describe('When checking if subscriptions exist', async function () {
206 const existPath = path + '/exist'
207
208 it('Should fail with a non authenticated user', async function () {
209 await makeGetRequest({
210 url: server.url,
211 path: existPath,
212 statusCodeExpected: 401
213 })
214 })
215
216 it('Should fail with bad URIs', async function () {
217 await makeGetRequest({
218 url: server.url,
219 path: existPath,
220 query: { uris: 'toto' },
221 token: server.accessToken,
222 statusCodeExpected: 400
223 })
224
225 await makeGetRequest({
226 url: server.url,
227 path: existPath,
228 query: { 'uris[]': 1 },
229 token: server.accessToken,
230 statusCodeExpected: 400
231 })
232 })
233
234 it('Should succeed with the correct parameters', async function () {
235 await makeGetRequest({
236 url: server.url,
237 path: existPath,
238 query: { 'uris[]': 'coucou@localhost:9001' },
239 token: server.accessToken,
240 statusCodeExpected: 200
241 })
242 })
243 })
244
205 describe('When removing a subscription', function () { 245 describe('When removing a subscription', function () {
206 it('Should fail with a non authenticated user', async function () { 246 it('Should fail with a non authenticated user', async function () {
207 await makeDeleteRequest({ 247 await makeDeleteRequest({
diff --git a/server/tests/api/users/user-subscriptions.ts b/server/tests/api/users/user-subscriptions.ts
index cb7d94b0b..65b80540c 100644
--- a/server/tests/api/users/user-subscriptions.ts
+++ b/server/tests/api/users/user-subscriptions.ts
@@ -12,7 +12,7 @@ import {
12 listUserSubscriptions, 12 listUserSubscriptions,
13 listUserSubscriptionVideos, 13 listUserSubscriptionVideos,
14 removeUserSubscription, 14 removeUserSubscription,
15 getUserSubscription 15 getUserSubscription, areSubscriptionsExist
16} from '../../utils/users/user-subscriptions' 16} from '../../utils/users/user-subscriptions'
17 17
18const expect = chai.expect 18const expect = chai.expect
@@ -128,6 +128,23 @@ describe('Test users subscriptions', function () {
128 } 128 }
129 }) 129 })
130 130
131 it('Should return the existing subscriptions', async function () {
132 const uris = [
133 'user3_channel@localhost:9003',
134 'root2_channel@localhost:9001',
135 'root_channel@localhost:9001',
136 'user3_channel@localhost:9001'
137 ]
138
139 const res = await areSubscriptionsExist(servers[ 0 ].url, users[ 0 ].accessToken, uris)
140 const body = res.body
141
142 expect(body['user3_channel@localhost:9003']).to.be.true
143 expect(body['root2_channel@localhost:9001']).to.be.false
144 expect(body['root_channel@localhost:9001']).to.be.true
145 expect(body['user3_channel@localhost:9001']).to.be.false
146 })
147
131 it('Should list subscription videos', async function () { 148 it('Should list subscription videos', async function () {
132 { 149 {
133 const res = await listUserSubscriptionVideos(servers[0].url, servers[0].accessToken) 150 const res = await listUserSubscriptionVideos(servers[0].url, servers[0].accessToken)
diff --git a/server/tests/utils/users/user-subscriptions.ts b/server/tests/utils/users/user-subscriptions.ts
index 852f590cf..b0e7da7cc 100644
--- a/server/tests/utils/users/user-subscriptions.ts
+++ b/server/tests/utils/users/user-subscriptions.ts
@@ -58,9 +58,22 @@ function removeUserSubscription (url: string, token: string, uri: string, status
58 }) 58 })
59} 59}
60 60
61function areSubscriptionsExist (url: string, token: string, uris: string[], statusCodeExpected = 200) {
62 const path = '/api/v1/users/me/subscriptions/exist'
63
64 return makeGetRequest({
65 url,
66 path,
67 query: { 'uris[]': uris },
68 token,
69 statusCodeExpected
70 })
71}
72
61// --------------------------------------------------------------------------- 73// ---------------------------------------------------------------------------
62 74
63export { 75export {
76 areSubscriptionsExist,
64 addUserSubscription, 77 addUserSubscription,
65 listUserSubscriptions, 78 listUserSubscriptions,
66 getUserSubscription, 79 getUserSubscription,