aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-10-19 09:44:43 +0200
committerChocobozzz <me@florianbigard.com>2021-10-20 09:25:44 +0200
commit4beda9e12adc7b1f3b178cecd6863ebf3cf431f1 (patch)
tree6244a10b286d66c6dcd7799aee630670d0493781 /server
parent9593a78ae1368a9ad8bb11044fce6fde2892701a (diff)
downloadPeerTube-4beda9e12adc7b1f3b178cecd6863ebf3cf431f1.tar.gz
PeerTube-4beda9e12adc7b1f3b178cecd6863ebf3cf431f1.tar.zst
PeerTube-4beda9e12adc7b1f3b178cecd6863ebf3cf431f1.zip
Add ability to view my followers
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/accounts.ts33
-rw-r--r--server/controllers/api/server/follows.ts4
-rw-r--r--server/controllers/api/users/my-subscriptions.ts2
-rw-r--r--server/controllers/api/video-channel.ts55
-rw-r--r--server/initializers/constants.ts3
-rw-r--r--server/middlewares/validators/sort.ts5
-rw-r--r--server/middlewares/validators/users.ts22
-rw-r--r--server/middlewares/validators/videos/video-channels.ts16
-rw-r--r--server/models/actor/actor-follow.ts22
-rw-r--r--server/models/video/video-channel.ts24
-rw-r--r--server/tests/api/check-params/users.ts28
-rw-r--r--server/tests/api/check-params/video-channels.ts28
-rw-r--r--server/tests/api/users/user-subscriptions.ts156
13 files changed, 349 insertions, 49 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 75679b0f4..77edfa7c2 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,5 +1,6 @@
1import express from 'express' 1import express from 'express'
2import { pickCommonVideoQuery } from '@server/helpers/query' 2import { pickCommonVideoQuery } from '@server/helpers/query'
3import { ActorFollowModel } from '@server/models/actor/actor-follow'
3import { getServerActor } from '@server/models/application/application' 4import { getServerActor } from '@server/models/application/application'
4import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 5import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
5import { getFormattedObjects } from '../../helpers/utils' 6import { getFormattedObjects } from '../../helpers/utils'
@@ -20,6 +21,7 @@ import {
20} from '../../middlewares' 21} from '../../middlewares'
21import { 22import {
22 accountNameWithHostGetValidator, 23 accountNameWithHostGetValidator,
24 accountsFollowersSortValidator,
23 accountsSortValidator, 25 accountsSortValidator,
24 ensureAuthUserOwnsAccountValidator, 26 ensureAuthUserOwnsAccountValidator,
25 videoChannelsSortValidator, 27 videoChannelsSortValidator,
@@ -93,6 +95,17 @@ accountsRouter.get('/:accountName/ratings',
93 asyncMiddleware(listAccountRatings) 95 asyncMiddleware(listAccountRatings)
94) 96)
95 97
98accountsRouter.get('/:accountName/followers',
99 authenticate,
100 asyncMiddleware(accountNameWithHostGetValidator),
101 ensureAuthUserOwnsAccountValidator,
102 paginationValidator,
103 accountsFollowersSortValidator,
104 setDefaultSort,
105 setDefaultPagination,
106 asyncMiddleware(listAccountFollowers)
107)
108
96// --------------------------------------------------------------------------- 109// ---------------------------------------------------------------------------
97 110
98export { 111export {
@@ -127,7 +140,7 @@ async function listAccountChannels (req: express.Request, res: express.Response)
127 search: req.query.search 140 search: req.query.search
128 } 141 }
129 142
130 const resultList = await VideoChannelModel.listByAccount(options) 143 const resultList = await VideoChannelModel.listByAccountForAPI(options)
131 144
132 return res.json(getFormattedObjects(resultList.data, resultList.total)) 145 return res.json(getFormattedObjects(resultList.data, resultList.total))
133} 146}
@@ -195,3 +208,21 @@ async function listAccountRatings (req: express.Request, res: express.Response)
195 }) 208 })
196 return res.json(getFormattedObjects(resultList.rows, resultList.count)) 209 return res.json(getFormattedObjects(resultList.rows, resultList.count))
197} 210}
211
212async function listAccountFollowers (req: express.Request, res: express.Response) {
213 const account = res.locals.account
214
215 const channels = await VideoChannelModel.listAllByAccount(account.id)
216 const actorIds = [ account.actorId ].concat(channels.map(c => c.actorId))
217
218 const resultList = await ActorFollowModel.listFollowersForApi({
219 actorIds,
220 start: req.query.start,
221 count: req.query.count,
222 sort: req.query.sort,
223 search: req.query.search,
224 state: 'accepted',
225 })
226
227 return res.json(getFormattedObjects(resultList.data, resultList.total))
228}
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index 76ed75186..c613386b2 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -98,7 +98,7 @@ export {
98 98
99async function listFollowing (req: express.Request, res: express.Response) { 99async function listFollowing (req: express.Request, res: express.Response) {
100 const serverActor = await getServerActor() 100 const serverActor = await getServerActor()
101 const resultList = await ActorFollowModel.listFollowingForApi({ 101 const resultList = await ActorFollowModel.listInstanceFollowingForApi({
102 id: serverActor.id, 102 id: serverActor.id,
103 start: req.query.start, 103 start: req.query.start,
104 count: req.query.count, 104 count: req.query.count,
@@ -114,7 +114,7 @@ async function listFollowing (req: express.Request, res: express.Response) {
114async function listFollowers (req: express.Request, res: express.Response) { 114async function listFollowers (req: express.Request, res: express.Response) {
115 const serverActor = await getServerActor() 115 const serverActor = await getServerActor()
116 const resultList = await ActorFollowModel.listFollowersForApi({ 116 const resultList = await ActorFollowModel.listFollowersForApi({
117 actorId: serverActor.id, 117 actorIds: [ serverActor.id ],
118 start: req.query.start, 118 start: req.query.start,
119 count: req.query.count, 119 count: req.query.count,
120 sort: req.query.sort, 120 sort: req.query.sort,
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index e3c0cf089..b2b441673 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -95,7 +95,7 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons
95 return { name, host, uri: u } 95 return { name, host, uri: u }
96 }) 96 })
97 97
98 const results = await ActorFollowModel.listSubscribedIn(user.Account.Actor.id, handles) 98 const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, handles)
99 99
100 const existObject: { [id: string ]: boolean } = {} 100 const existObject: { [id: string ]: boolean } = {}
101 for (const handle of handles) { 101 for (const handle of handles) {
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index b79dc5933..f370c7004 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -1,6 +1,7 @@
1import express from 'express' 1import express from 'express'
2import { pickCommonVideoQuery } from '@server/helpers/query' 2import { pickCommonVideoQuery } from '@server/helpers/query'
3import { Hooks } from '@server/lib/plugins/hooks' 3import { Hooks } from '@server/lib/plugins/hooks'
4import { ActorFollowModel } from '@server/models/actor/actor-follow'
4import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
5import { MChannelBannerAccountDefault } from '@server/types/models' 6import { MChannelBannerAccountDefault } from '@server/types/models'
6import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 7import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
@@ -33,7 +34,13 @@ import {
33 videoChannelsUpdateValidator, 34 videoChannelsUpdateValidator,
34 videoPlaylistsSortValidator 35 videoPlaylistsSortValidator
35} from '../../middlewares' 36} from '../../middlewares'
36import { videoChannelsListValidator, videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators' 37import {
38 ensureAuthUserOwnsChannelValidator,
39 videoChannelsFollowersSortValidator,
40 videoChannelsListValidator,
41 videoChannelsNameWithHostValidator,
42 videosSortValidator
43} from '../../middlewares/validators'
37import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' 44import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image'
38import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' 45import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
39import { AccountModel } from '../../models/account/account' 46import { AccountModel } from '../../models/account/account'
@@ -65,8 +72,8 @@ videoChannelRouter.post('/',
65videoChannelRouter.post('/:nameWithHost/avatar/pick', 72videoChannelRouter.post('/:nameWithHost/avatar/pick',
66 authenticate, 73 authenticate,
67 reqAvatarFile, 74 reqAvatarFile,
68 // Check the rights 75 asyncMiddleware(videoChannelsNameWithHostValidator),
69 asyncMiddleware(videoChannelsUpdateValidator), 76 ensureAuthUserOwnsChannelValidator,
70 updateAvatarValidator, 77 updateAvatarValidator,
71 asyncMiddleware(updateVideoChannelAvatar) 78 asyncMiddleware(updateVideoChannelAvatar)
72) 79)
@@ -74,29 +81,31 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
74videoChannelRouter.post('/:nameWithHost/banner/pick', 81videoChannelRouter.post('/:nameWithHost/banner/pick',
75 authenticate, 82 authenticate,
76 reqBannerFile, 83 reqBannerFile,
77 // Check the rights 84 asyncMiddleware(videoChannelsNameWithHostValidator),
78 asyncMiddleware(videoChannelsUpdateValidator), 85 ensureAuthUserOwnsChannelValidator,
79 updateBannerValidator, 86 updateBannerValidator,
80 asyncMiddleware(updateVideoChannelBanner) 87 asyncMiddleware(updateVideoChannelBanner)
81) 88)
82 89
83videoChannelRouter.delete('/:nameWithHost/avatar', 90videoChannelRouter.delete('/:nameWithHost/avatar',
84 authenticate, 91 authenticate,
85 // Check the rights 92 asyncMiddleware(videoChannelsNameWithHostValidator),
86 asyncMiddleware(videoChannelsUpdateValidator), 93 ensureAuthUserOwnsChannelValidator,
87 asyncMiddleware(deleteVideoChannelAvatar) 94 asyncMiddleware(deleteVideoChannelAvatar)
88) 95)
89 96
90videoChannelRouter.delete('/:nameWithHost/banner', 97videoChannelRouter.delete('/:nameWithHost/banner',
91 authenticate, 98 authenticate,
92 // Check the rights 99 asyncMiddleware(videoChannelsNameWithHostValidator),
93 asyncMiddleware(videoChannelsUpdateValidator), 100 ensureAuthUserOwnsChannelValidator,
94 asyncMiddleware(deleteVideoChannelBanner) 101 asyncMiddleware(deleteVideoChannelBanner)
95) 102)
96 103
97videoChannelRouter.put('/:nameWithHost', 104videoChannelRouter.put('/:nameWithHost',
98 authenticate, 105 authenticate,
99 asyncMiddleware(videoChannelsUpdateValidator), 106 asyncMiddleware(videoChannelsNameWithHostValidator),
107 ensureAuthUserOwnsChannelValidator,
108 videoChannelsUpdateValidator,
100 asyncRetryTransactionMiddleware(updateVideoChannel) 109 asyncRetryTransactionMiddleware(updateVideoChannel)
101) 110)
102 111
@@ -132,6 +141,17 @@ videoChannelRouter.get('/:nameWithHost/videos',
132 asyncMiddleware(listVideoChannelVideos) 141 asyncMiddleware(listVideoChannelVideos)
133) 142)
134 143
144videoChannelRouter.get('/:nameWithHost/followers',
145 authenticate,
146 asyncMiddleware(videoChannelsNameWithHostValidator),
147 ensureAuthUserOwnsChannelValidator,
148 paginationValidator,
149 videoChannelsFollowersSortValidator,
150 setDefaultSort,
151 setDefaultPagination,
152 asyncMiddleware(listVideoChannelFollowers)
153)
154
135// --------------------------------------------------------------------------- 155// ---------------------------------------------------------------------------
136 156
137export { 157export {
@@ -332,3 +352,18 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
332 352
333 return res.json(getFormattedObjects(resultList.data, resultList.total)) 353 return res.json(getFormattedObjects(resultList.data, resultList.total))
334} 354}
355
356async function listVideoChannelFollowers (req: express.Request, res: express.Response) {
357 const channel = res.locals.videoChannel
358
359 const resultList = await ActorFollowModel.listFollowersForApi({
360 actorIds: [ channel.actorId ],
361 start: req.query.start,
362 count: req.query.count,
363 sort: req.query.sort,
364 search: req.query.search,
365 state: 'accepted',
366 })
367
368 return res.json(getFormattedObjects(resultList.data, resultList.total))
369}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 029984559..dcbad9264 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -69,8 +69,11 @@ const SORTABLE_COLUMNS = {
69 69
70 VIDEO_RATES: [ 'createdAt' ], 70 VIDEO_RATES: [ 'createdAt' ],
71 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], 71 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
72
72 INSTANCE_FOLLOWERS: [ 'createdAt', 'state', 'score' ], 73 INSTANCE_FOLLOWERS: [ 'createdAt', 'state', 'score' ],
73 INSTANCE_FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ], 74 INSTANCE_FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ],
75 ACCOUNT_FOLLOWERS: [ 'createdAt' ],
76 CHANNEL_FOLLOWERS: [ 'createdAt' ],
74 77
75 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ], 78 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ],
76 79
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index ce8df8fee..3ba668460 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -53,6 +53,9 @@ const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS)
53const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) 53const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS)
54const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) 54const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES)
55 55
56const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS)
57const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS)
58
56// --------------------------------------------------------------------------- 59// ---------------------------------------------------------------------------
57 60
58export { 61export {
@@ -79,5 +82,7 @@ export {
79 videoPlaylistsSortValidator, 82 videoPlaylistsSortValidator,
80 videoRedundanciesSortValidator, 83 videoRedundanciesSortValidator,
81 videoPlaylistsSearchSortValidator, 84 videoPlaylistsSearchSortValidator,
85 accountsFollowersSortValidator,
86 videoChannelsFollowersSortValidator,
82 pluginsSortValidator 87 pluginsSortValidator
83} 88}
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index c06b85862..c6eeeaf18 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -3,9 +3,7 @@ import { body, param, query } from 'express-validator'
3import { omit } from 'lodash' 3import { omit } from 'lodash'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { MUserDefault } from '@server/types/models' 5import { MUserDefault } from '@server/types/models'
6import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' 6import { HttpStatusCode, UserRegister, UserRole } from '@shared/models'
7import { UserRole } from '../../../shared/models/users'
8import { UserRegister } from '../../../shared/models/users/user-register.model'
9import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' 7import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
10import { isThemeNameValid } from '../../helpers/custom-validators/plugins' 8import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
11import { 9import {
@@ -462,7 +460,22 @@ const ensureAuthUserOwnsAccountValidator = [
462 if (res.locals.account.id !== user.Account.id) { 460 if (res.locals.account.id !== user.Account.id) {
463 return res.fail({ 461 return res.fail({
464 status: HttpStatusCode.FORBIDDEN_403, 462 status: HttpStatusCode.FORBIDDEN_403,
465 message: 'Only owner can access ratings list.' 463 message: 'Only owner of this account can access this ressource.'
464 })
465 }
466
467 return next()
468 }
469]
470
471const ensureAuthUserOwnsChannelValidator = [
472 (req: express.Request, res: express.Response, next: express.NextFunction) => {
473 const user = res.locals.oauth.token.User
474
475 if (res.locals.videoChannel.Account.userId !== user.id) {
476 return res.fail({
477 status: HttpStatusCode.FORBIDDEN_403,
478 message: 'Only owner of this video channel can access this ressource'
466 }) 479 })
467 } 480 }
468 481
@@ -506,6 +519,7 @@ export {
506 usersVerifyEmailValidator, 519 usersVerifyEmailValidator,
507 userAutocompleteValidator, 520 userAutocompleteValidator,
508 ensureAuthUserOwnsAccountValidator, 521 ensureAuthUserOwnsAccountValidator,
522 ensureAuthUserOwnsChannelValidator,
509 ensureCanManageUser 523 ensureCanManageUser
510} 524}
511 525
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index fc717abf6..ec107fa51 100644
--- a/server/middlewares/validators/videos/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -65,22 +65,6 @@ const videoChannelsUpdateValidator = [
65 logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body }) 65 logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body })
66 66
67 if (areValidationErrors(req, res)) return 67 if (areValidationErrors(req, res)) return
68 if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
69
70 // We need to make additional checks
71 if (res.locals.videoChannel.Actor.isOwned() === false) {
72 return res.fail({
73 status: HttpStatusCode.FORBIDDEN_403,
74 message: 'Cannot update video channel of another server'
75 })
76 }
77
78 if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) {
79 return res.fail({
80 status: HttpStatusCode.FORBIDDEN_403,
81 message: 'Cannot update video channel of another user'
82 })
83 }
84 68
85 return next() 69 return next()
86 } 70 }
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts
index fc1cc7499..d6a2387a5 100644
--- a/server/models/actor/actor-follow.ts
+++ b/server/models/actor/actor-follow.ts
@@ -143,7 +143,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
143 * @deprecated Use `findOrCreateCustom` instead 143 * @deprecated Use `findOrCreateCustom` instead
144 */ 144 */
145 static findOrCreate (): any { 145 static findOrCreate (): any {
146 throw new Error('Should not be called') 146 throw new Error('Must not be called')
147 } 147 }
148 148
149 // findOrCreate has issues with actor follow hooks 149 // findOrCreate has issues with actor follow hooks
@@ -288,7 +288,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
288 return ActorFollowModel.findOne(query) 288 return ActorFollowModel.findOne(query)
289 } 289 }
290 290
291 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { 291 static listSubscriptionsOf (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> {
292 const whereTab = targets 292 const whereTab = targets
293 .map(t => { 293 .map(t => {
294 if (t.host) { 294 if (t.host) {
@@ -348,7 +348,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
348 return ActorFollowModel.findAll(query) 348 return ActorFollowModel.findAll(query)
349 } 349 }
350 350
351 static listFollowingForApi (options: { 351 static listInstanceFollowingForApi (options: {
352 id: number 352 id: number
353 start: number 353 start: number
354 count: number 354 count: number
@@ -415,7 +415,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
415 } 415 }
416 416
417 static listFollowersForApi (options: { 417 static listFollowersForApi (options: {
418 actorId: number 418 actorIds: number[]
419 start: number 419 start: number
420 count: number 420 count: number
421 sort: string 421 sort: string
@@ -423,7 +423,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
423 actorType?: ActivityPubActorType 423 actorType?: ActivityPubActorType
424 search?: string 424 search?: string
425 }) { 425 }) {
426 const { actorId, start, count, sort, search, state, actorType } = options 426 const { actorIds, start, count, sort, search, state, actorType } = options
427 427
428 const followWhere = state ? { state } : {} 428 const followWhere = state ? { state } : {}
429 const followerWhere: WhereOptions = {} 429 const followerWhere: WhereOptions = {}
@@ -452,20 +452,16 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
452 model: ActorModel, 452 model: ActorModel,
453 required: true, 453 required: true,
454 as: 'ActorFollower', 454 as: 'ActorFollower',
455 where: followerWhere, 455 where: followerWhere
456 include: [
457 {
458 model: ServerModel,
459 required: true
460 }
461 ]
462 }, 456 },
463 { 457 {
464 model: ActorModel, 458 model: ActorModel,
465 as: 'ActorFollowing', 459 as: 'ActorFollowing',
466 required: true, 460 required: true,
467 where: { 461 where: {
468 id: actorId 462 id: {
463 [Op.in]: actorIds
464 }
469 } 465 }
470 } 466 }
471 ] 467 ]
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 8ccb818b3..a151ad61c 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -26,7 +26,7 @@ import {
26 isVideoChannelDisplayNameValid, 26 isVideoChannelDisplayNameValid,
27 isVideoChannelSupportValid 27 isVideoChannelSupportValid
28} from '../../helpers/custom-validators/video-channels' 28} from '../../helpers/custom-validators/video-channels'
29import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 29import { CONSTRAINTS_FIELDS, VIDEO_CHANNELS, WEBSERVER } from '../../initializers/constants'
30import { sendDeleteActor } from '../../lib/activitypub/send' 30import { sendDeleteActor } from '../../lib/activitypub/send'
31import { 31import {
32 MChannelActor, 32 MChannelActor,
@@ -527,7 +527,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
527 }) 527 })
528 } 528 }
529 529
530 static listByAccount (options: { 530 static listByAccountForAPI (options: {
531 accountId: number 531 accountId: number
532 start: number 532 start: number
533 count: number 533 count: number
@@ -582,6 +582,26 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
582 }) 582 })
583 } 583 }
584 584
585
586 static listAllByAccount (accountId: number) {
587 const query = {
588 limit: VIDEO_CHANNELS.MAX_PER_USER,
589 include: [
590 {
591 attributes: [],
592 model: AccountModel,
593 where: {
594 id: accountId
595 },
596 required: true
597 }
598 ]
599 }
600
601 return VideoChannelModel.findAll(query)
602 }
603
604
585 static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise<MChannelBannerAccountDefault> { 605 static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise<MChannelBannerAccountDefault> {
586 return VideoChannelModel.unscoped() 606 return VideoChannelModel.unscoped()
587 .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) 607 .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ])
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 58b360f92..517e2f423 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -840,6 +840,34 @@ describe('Test users API validators', function () {
840 }) 840 })
841 }) 841 })
842 842
843 describe('When getting my global followers', function () {
844 const path = '/api/v1/accounts/user1/followers'
845
846 it('Should fail with a bad start pagination', async function () {
847 await checkBadStartPagination(server.url, path, userToken)
848 })
849
850 it('Should fail with a bad count pagination', async function () {
851 await checkBadCountPagination(server.url, path, userToken)
852 })
853
854 it('Should fail with an incorrect sort', async function () {
855 await checkBadSortPagination(server.url, path, userToken)
856 })
857
858 it('Should fail with a unauthenticated user', async function () {
859 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
860 })
861
862 it('Should fail with a another user', async function () {
863 await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
864 })
865
866 it('Should succeed with the correct params', async function () {
867 await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 })
868 })
869 })
870
843 describe('When blocking/unblocking/removing user', function () { 871 describe('When blocking/unblocking/removing user', function () {
844 872
845 it('Should fail with an incorrect id', async function () { 873 it('Should fail with an incorrect id', async function () {
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts
index 2e63916d4..e86c315fa 100644
--- a/server/tests/api/check-params/video-channels.ts
+++ b/server/tests/api/check-params/video-channels.ts
@@ -321,6 +321,34 @@ describe('Test video channels API validator', function () {
321 }) 321 })
322 }) 322 })
323 323
324 describe('When getting channel followers', function () {
325 const path = '/api/v1/video-channels/super_channel/followers'
326
327 it('Should fail with a bad start pagination', async function () {
328 await checkBadStartPagination(server.url, path, server.accessToken)
329 })
330
331 it('Should fail with a bad count pagination', async function () {
332 await checkBadCountPagination(server.url, path, server.accessToken)
333 })
334
335 it('Should fail with an incorrect sort', async function () {
336 await checkBadSortPagination(server.url, path, server.accessToken)
337 })
338
339 it('Should fail with a unauthenticated user', async function () {
340 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
341 })
342
343 it('Should fail with a another user', async function () {
344 await makeGetRequest({ url: server.url, path, token: accessTokenUser, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
345 })
346
347 it('Should succeed with the correct params', async function () {
348 await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
349 })
350 })
351
324 describe('When deleting a video channel', function () { 352 describe('When deleting a video channel', function () {
325 it('Should fail with a non authenticated user', async function () { 353 it('Should fail with a non authenticated user', async function () {
326 await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) 354 await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
diff --git a/server/tests/api/users/user-subscriptions.ts b/server/tests/api/users/user-subscriptions.ts
index 441f70d07..b49367be6 100644
--- a/server/tests/api/users/user-subscriptions.ts
+++ b/server/tests/api/users/user-subscriptions.ts
@@ -368,6 +368,162 @@ describe('Test users subscriptions', function () {
368 } 368 }
369 }) 369 })
370 370
371 it('Should follow user channels of server 3 by root of server 3', async function () {
372 this.timeout(60000)
373
374 await servers[2].channels.create({ token: users[2].accessToken, attributes: { name: 'user3_channel2' } })
375
376 await servers[2].subscriptions.add({ token: servers[2].accessToken, targetUri: 'user3_channel@localhost:' + servers[2].port })
377 await servers[2].subscriptions.add({ token: servers[2].accessToken, targetUri: 'user3_channel2@localhost:' + servers[2].port })
378
379 await waitJobs(servers)
380 })
381
382 it('Should list user 3 followers', async function () {
383 {
384 const { total, data } = await servers[2].accounts.listFollowers({
385 token: users[2].accessToken,
386 accountName: 'user3',
387 start: 0,
388 count: 5,
389 sort: 'createdAt'
390 })
391
392 expect(total).to.equal(3)
393 expect(data[0].following.host).to.equal(servers[2].host)
394 expect(data[0].following.name).to.equal('user3_channel')
395 expect(data[0].follower.host).to.equal(servers[0].host)
396 expect(data[0].follower.name).to.equal('user1')
397
398 expect(data[1].following.host).to.equal(servers[2].host)
399 expect(data[1].following.name).to.equal('user3_channel')
400 expect(data[1].follower.host).to.equal(servers[2].host)
401 expect(data[1].follower.name).to.equal('root')
402
403 expect(data[2].following.host).to.equal(servers[2].host)
404 expect(data[2].following.name).to.equal('user3_channel2')
405 expect(data[2].follower.host).to.equal(servers[2].host)
406 expect(data[2].follower.name).to.equal('root')
407 }
408
409 {
410 const { total, data } = await servers[2].accounts.listFollowers({
411 token: users[2].accessToken,
412 accountName: 'user3',
413 start: 0,
414 count: 1,
415 sort: '-createdAt'
416 })
417
418 expect(total).to.equal(3)
419 expect(data[0].following.host).to.equal(servers[2].host)
420 expect(data[0].following.name).to.equal('user3_channel2')
421 expect(data[0].follower.host).to.equal(servers[2].host)
422 expect(data[0].follower.name).to.equal('root')
423 }
424
425 {
426 const { total, data } = await servers[2].accounts.listFollowers({
427 token: users[2].accessToken,
428 accountName: 'user3',
429 start: 1,
430 count: 1,
431 sort: '-createdAt'
432 })
433
434 expect(total).to.equal(3)
435 expect(data[0].following.host).to.equal(servers[2].host)
436 expect(data[0].following.name).to.equal('user3_channel')
437 expect(data[0].follower.host).to.equal(servers[2].host)
438 expect(data[0].follower.name).to.equal('root')
439 }
440
441 {
442 const { total, data } = await servers[2].accounts.listFollowers({
443 token: users[2].accessToken,
444 accountName: 'user3',
445 search: 'user1',
446 sort: '-createdAt'
447 })
448
449 expect(total).to.equal(1)
450 expect(data[0].following.host).to.equal(servers[2].host)
451 expect(data[0].following.name).to.equal('user3_channel')
452 expect(data[0].follower.host).to.equal(servers[0].host)
453 expect(data[0].follower.name).to.equal('user1')
454 }
455 })
456
457 it('Should list user3_channel followers', async function () {
458 {
459 const { total, data } = await servers[2].channels.listFollowers({
460 token: users[2].accessToken,
461 channelName: 'user3_channel',
462 start: 0,
463 count: 5,
464 sort: 'createdAt'
465 })
466
467 expect(total).to.equal(2)
468 expect(data[0].following.host).to.equal(servers[2].host)
469 expect(data[0].following.name).to.equal('user3_channel')
470 expect(data[0].follower.host).to.equal(servers[0].host)
471 expect(data[0].follower.name).to.equal('user1')
472
473 expect(data[1].following.host).to.equal(servers[2].host)
474 expect(data[1].following.name).to.equal('user3_channel')
475 expect(data[1].follower.host).to.equal(servers[2].host)
476 expect(data[1].follower.name).to.equal('root')
477 }
478
479 {
480 const { total, data } = await servers[2].channels.listFollowers({
481 token: users[2].accessToken,
482 channelName: 'user3_channel',
483 start: 0,
484 count: 1,
485 sort: '-createdAt'
486 })
487
488 expect(total).to.equal(2)
489 expect(data[0].following.host).to.equal(servers[2].host)
490 expect(data[0].following.name).to.equal('user3_channel')
491 expect(data[0].follower.host).to.equal(servers[2].host)
492 expect(data[0].follower.name).to.equal('root')
493 }
494
495 {
496 const { total, data } = await servers[2].channels.listFollowers({
497 token: users[2].accessToken,
498 channelName: 'user3_channel',
499 start: 1,
500 count: 1,
501 sort: '-createdAt'
502 })
503
504 expect(total).to.equal(2)
505 expect(data[0].following.host).to.equal(servers[2].host)
506 expect(data[0].following.name).to.equal('user3_channel')
507 expect(data[0].follower.host).to.equal(servers[0].host)
508 expect(data[0].follower.name).to.equal('root')
509 }
510
511 {
512 const { total, data } = await servers[2].channels.listFollowers({
513 token: users[2].accessToken,
514 channelName: 'user3_channel',
515 search: 'user1',
516 sort: '-createdAt'
517 })
518
519 expect(total).to.equal(1)
520 expect(data[0].following.host).to.equal(servers[2].host)
521 expect(data[0].following.name).to.equal('user3_channel')
522 expect(data[0].follower.host).to.equal(servers[0].host)
523 expect(data[0].follower.name).to.equal('user1')
524 }
525 })
526
371 after(async function () { 527 after(async function () {
372 await cleanupTests(servers) 528 await cleanupTests(servers)
373 }) 529 })