diff options
author | Chocobozzz <me@florianbigard.com> | 2018-08-16 15:25:20 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-08-27 09:41:54 +0200 |
commit | 06a05d5f4784a7cbb27aa1188385b5679845dad8 (patch) | |
tree | ac197f3ed0768529456225ad76c912f22bc55e29 | |
parent | 4bda2e47bbc937c401ddcf14c1be53c70481a294 (diff) | |
download | PeerTube-06a05d5f4784a7cbb27aa1188385b5679845dad8.tar.gz PeerTube-06a05d5f4784a7cbb27aa1188385b5679845dad8.tar.zst PeerTube-06a05d5f4784a7cbb27aa1188385b5679845dad8.zip |
Add subscriptions endpoints to REST API
36 files changed, 1038 insertions, 93 deletions
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts index a106df717..20bd20ed4 100644 --- a/server/controllers/activitypub/inbox.ts +++ b/server/controllers/activitypub/inbox.ts | |||
@@ -3,9 +3,10 @@ import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, RootActi | |||
3 | import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' | 3 | import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
5 | import { processActivities } from '../../lib/activitypub/process/process' | 5 | import { processActivities } from '../../lib/activitypub/process/process' |
6 | import { asyncMiddleware, checkSignature, localAccountValidator, signatureValidator } from '../../middlewares' | 6 | import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChannelValidator, signatureValidator } from '../../middlewares' |
7 | import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' | 7 | import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' |
8 | import { ActorModel } from '../../models/activitypub/actor' | 8 | import { VideoChannelModel } from '../../models/video/video-channel' |
9 | import { AccountModel } from '../../models/account/account' | ||
9 | 10 | ||
10 | const inboxRouter = express.Router() | 11 | const inboxRouter = express.Router() |
11 | 12 | ||
@@ -23,6 +24,13 @@ inboxRouter.post('/accounts/:name/inbox', | |||
23 | asyncMiddleware(activityPubValidator), | 24 | asyncMiddleware(activityPubValidator), |
24 | asyncMiddleware(inboxController) | 25 | asyncMiddleware(inboxController) |
25 | ) | 26 | ) |
27 | inboxRouter.post('/video-channels/:name/inbox', | ||
28 | signatureValidator, | ||
29 | asyncMiddleware(checkSignature), | ||
30 | asyncMiddleware(localVideoChannelValidator), | ||
31 | asyncMiddleware(activityPubValidator), | ||
32 | asyncMiddleware(inboxController) | ||
33 | ) | ||
26 | 34 | ||
27 | // --------------------------------------------------------------------------- | 35 | // --------------------------------------------------------------------------- |
28 | 36 | ||
@@ -49,16 +57,16 @@ async function inboxController (req: express.Request, res: express.Response, nex | |||
49 | activities = activities.filter(a => isActivityValid(a)) | 57 | activities = activities.filter(a => isActivityValid(a)) |
50 | logger.debug('We keep %d activities.', activities.length, { activities }) | 58 | logger.debug('We keep %d activities.', activities.length, { activities }) |
51 | 59 | ||
52 | let specificActor: ActorModel = undefined | 60 | let accountOrChannel: VideoChannelModel | AccountModel |
53 | if (res.locals.account) { | 61 | if (res.locals.account) { |
54 | specificActor = res.locals.account | 62 | accountOrChannel = res.locals.account |
55 | } else if (res.locals.videoChannel) { | 63 | } else if (res.locals.videoChannel) { |
56 | specificActor = res.locals.videoChannel | 64 | accountOrChannel = res.locals.videoChannel |
57 | } | 65 | } |
58 | 66 | ||
59 | logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor.url) | 67 | logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor.url) |
60 | 68 | ||
61 | await processActivities(activities, res.locals.signature.actor, specificActor) | 69 | await processActivities(activities, res.locals.signature.actor, accountOrChannel ? accountOrChannel.Actor : undefined) |
62 | 70 | ||
63 | res.status(204).end() | 71 | res.status(204).end() |
64 | } | 72 | } |
diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts index ae7adcd4c..db69ae54b 100644 --- a/server/controllers/activitypub/outbox.ts +++ b/server/controllers/activitypub/outbox.ts | |||
@@ -5,11 +5,12 @@ import { activityPubCollectionPagination, activityPubContextify } from '../../he | |||
5 | import { logger } from '../../helpers/logger' | 5 | import { logger } from '../../helpers/logger' |
6 | import { announceActivityData, createActivityData } from '../../lib/activitypub/send' | 6 | import { announceActivityData, createActivityData } from '../../lib/activitypub/send' |
7 | import { buildAudience } from '../../lib/activitypub/audience' | 7 | import { buildAudience } from '../../lib/activitypub/audience' |
8 | import { asyncMiddleware, localAccountValidator } from '../../middlewares' | 8 | import { asyncMiddleware, localAccountValidator, localVideoChannelValidator } from '../../middlewares' |
9 | import { AccountModel } from '../../models/account/account' | 9 | import { AccountModel } from '../../models/account/account' |
10 | import { ActorModel } from '../../models/activitypub/actor' | 10 | import { ActorModel } from '../../models/activitypub/actor' |
11 | import { VideoModel } from '../../models/video/video' | 11 | import { VideoModel } from '../../models/video/video' |
12 | import { activityPubResponse } from './utils' | 12 | import { activityPubResponse } from './utils' |
13 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
13 | 14 | ||
14 | const outboxRouter = express.Router() | 15 | const outboxRouter = express.Router() |
15 | 16 | ||
@@ -18,6 +19,11 @@ outboxRouter.get('/accounts/:name/outbox', | |||
18 | asyncMiddleware(outboxController) | 19 | asyncMiddleware(outboxController) |
19 | ) | 20 | ) |
20 | 21 | ||
22 | outboxRouter.get('/video-channels/:name/outbox', | ||
23 | localVideoChannelValidator, | ||
24 | asyncMiddleware(outboxController) | ||
25 | ) | ||
26 | |||
21 | // --------------------------------------------------------------------------- | 27 | // --------------------------------------------------------------------------- |
22 | 28 | ||
23 | export { | 29 | export { |
@@ -27,9 +33,9 @@ export { | |||
27 | // --------------------------------------------------------------------------- | 33 | // --------------------------------------------------------------------------- |
28 | 34 | ||
29 | async function outboxController (req: express.Request, res: express.Response, next: express.NextFunction) { | 35 | async function outboxController (req: express.Request, res: express.Response, next: express.NextFunction) { |
30 | const account: AccountModel = res.locals.account | 36 | const accountOrVideoChannel: AccountModel | VideoChannelModel = res.locals.account || res.locals.videoChannel |
31 | const actor = account.Actor | 37 | const actor = accountOrVideoChannel.Actor |
32 | const actorOutboxUrl = account.Actor.url + '/outbox' | 38 | const actorOutboxUrl = actor.url + '/outbox' |
33 | 39 | ||
34 | logger.info('Receiving outbox request for %s.', actorOutboxUrl) | 40 | logger.info('Receiving outbox request for %s.', actorOutboxUrl) |
35 | 41 | ||
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 0117fc8c6..308970abc 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -78,6 +78,7 @@ async function listAccountVideos (req: express.Request, res: express.Response, n | |||
78 | start: req.query.start, | 78 | start: req.query.start, |
79 | count: req.query.count, | 79 | count: req.query.count, |
80 | sort: req.query.sort, | 80 | sort: req.query.sort, |
81 | includeLocalVideos: false, | ||
81 | categoryOneOf: req.query.categoryOneOf, | 82 | categoryOneOf: req.query.categoryOneOf, |
82 | licenceOneOf: req.query.licenceOneOf, | 83 | licenceOneOf: req.query.licenceOneOf, |
83 | languageOneOf: req.query.languageOneOf, | 84 | languageOneOf: req.query.languageOneOf, |
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts index f810c7452..7a7504b7d 100644 --- a/server/controllers/api/search.ts +++ b/server/controllers/api/search.ts | |||
@@ -36,7 +36,10 @@ export { searchRouter } | |||
36 | async function searchVideos (req: express.Request, res: express.Response) { | 36 | async function searchVideos (req: express.Request, res: express.Response) { |
37 | const query: VideosSearchQuery = req.query | 37 | const query: VideosSearchQuery = req.query |
38 | 38 | ||
39 | const options = Object.assign(query, { nsfw: buildNSFWFilter(res, query.nsfw) }) | 39 | const options = Object.assign(query, { |
40 | includeLocalVideos: true, | ||
41 | nsfw: buildNSFWFilter(res, query.nsfw) | ||
42 | }) | ||
40 | const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) | 43 | const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) |
41 | 44 | ||
42 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 45 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index e78361c9a..23308445f 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import { UserRight } from '../../../../shared/models/users' | 2 | import { UserRight } from '../../../../shared/models/users' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' | 4 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' |
5 | import { sequelizeTypescript } from '../../../initializers' | 5 | import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers' |
6 | import { sendUndoFollow } from '../../../lib/activitypub/send' | 6 | import { sendUndoFollow } from '../../../lib/activitypub/send' |
7 | import { | 7 | import { |
8 | asyncMiddleware, | 8 | asyncMiddleware, |
@@ -74,9 +74,16 @@ async function listFollowers (req: express.Request, res: express.Response, next: | |||
74 | 74 | ||
75 | async function followInstance (req: express.Request, res: express.Response, next: express.NextFunction) { | 75 | async function followInstance (req: express.Request, res: express.Response, next: express.NextFunction) { |
76 | const hosts = req.body.hosts as string[] | 76 | const hosts = req.body.hosts as string[] |
77 | const follower = await getServerActor() | ||
77 | 78 | ||
78 | for (const host of hosts) { | 79 | for (const host of hosts) { |
79 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload: { host } }) | 80 | const payload = { |
81 | host, | ||
82 | name: SERVER_ACTOR_NAME, | ||
83 | followerActorId: follower.id | ||
84 | } | ||
85 | |||
86 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) | ||
80 | .catch(err => logger.error('Cannot create follow job for %s.', host, err)) | 87 | .catch(err => logger.error('Cannot create follow job for %s.', host, err)) |
81 | } | 88 | } |
82 | 89 | ||
@@ -92,11 +99,5 @@ async function removeFollow (req: express.Request, res: express.Response, next: | |||
92 | await follow.destroy({ transaction: t }) | 99 | await follow.destroy({ transaction: t }) |
93 | }) | 100 | }) |
94 | 101 | ||
95 | // Destroy the actor that will destroy video channels, videos and video files too | ||
96 | // This could be long so don't wait this task | ||
97 | const following = follow.ActorFollowing | ||
98 | following.destroy() | ||
99 | .catch(err => logger.error('Cannot destroy actor that we do not follow anymore %s.', following.url, { err })) | ||
100 | |||
101 | return res.status(204).end() | 102 | return res.status(204).end() |
102 | } | 103 | } |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 105244ddd..608d439ac 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -29,7 +29,6 @@ import { usersAskResetPasswordValidator, usersBlockingValidator, usersResetPassw | |||
29 | import { UserModel } from '../../../models/account/user' | 29 | import { UserModel } from '../../../models/account/user' |
30 | import { OAuthTokenModel } from '../../../models/oauth/oauth-token' | 30 | import { OAuthTokenModel } from '../../../models/oauth/oauth-token' |
31 | import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' | 31 | import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' |
32 | import { videosRouter } from '../videos' | ||
33 | import { meRouter } from './me' | 32 | import { meRouter } from './me' |
34 | 33 | ||
35 | const auditLogger = auditLoggerFactory('users') | 34 | const auditLogger = auditLoggerFactory('users') |
@@ -41,7 +40,7 @@ const loginRateLimiter = new RateLimit({ | |||
41 | }) | 40 | }) |
42 | 41 | ||
43 | const usersRouter = express.Router() | 42 | const usersRouter = express.Router() |
44 | videosRouter.use('/', meRouter) | 43 | usersRouter.use('/', meRouter) |
45 | 44 | ||
46 | usersRouter.get('/', | 45 | usersRouter.get('/', |
47 | authenticate, | 46 | authenticate, |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 1e096a35d..403842163 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -7,23 +7,35 @@ import { sendUpdateActor } from '../../../lib/activitypub/send' | |||
7 | import { | 7 | import { |
8 | asyncMiddleware, | 8 | asyncMiddleware, |
9 | authenticate, | 9 | authenticate, |
10 | commonVideosFiltersValidator, | ||
10 | paginationValidator, | 11 | paginationValidator, |
11 | setDefaultPagination, | 12 | setDefaultPagination, |
12 | setDefaultSort, | 13 | setDefaultSort, |
14 | userSubscriptionAddValidator, | ||
15 | userSubscriptionRemoveValidator, | ||
13 | usersUpdateMeValidator, | 16 | usersUpdateMeValidator, |
14 | usersVideoRatingValidator | 17 | usersVideoRatingValidator |
15 | } from '../../../middlewares' | 18 | } from '../../../middlewares' |
16 | import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' | 19 | import { |
20 | deleteMeValidator, | ||
21 | userSubscriptionsSortValidator, | ||
22 | videoImportsSortValidator, | ||
23 | videosSortValidator | ||
24 | } from '../../../middlewares/validators' | ||
17 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 25 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
18 | import { UserModel } from '../../../models/account/user' | 26 | import { UserModel } from '../../../models/account/user' |
19 | import { VideoModel } from '../../../models/video/video' | 27 | import { VideoModel } from '../../../models/video/video' |
20 | import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' | 28 | import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' |
21 | import { createReqFiles } from '../../../helpers/express-utils' | 29 | import { buildNSFWFilter, createReqFiles } from '../../../helpers/express-utils' |
22 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' | 30 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' |
23 | import { updateAvatarValidator } from '../../../middlewares/validators/avatar' | 31 | import { updateAvatarValidator } from '../../../middlewares/validators/avatar' |
24 | import { updateActorAvatarFile } from '../../../lib/avatar' | 32 | import { updateActorAvatarFile } from '../../../lib/avatar' |
25 | import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' | 33 | import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' |
26 | import { VideoImportModel } from '../../../models/video/video-import' | 34 | import { VideoImportModel } from '../../../models/video/video-import' |
35 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | ||
36 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
37 | import { JobQueue } from '../../../lib/job-queue' | ||
38 | import { logger } from '../../../helpers/logger' | ||
27 | 39 | ||
28 | const auditLogger = auditLoggerFactory('users-me') | 40 | const auditLogger = auditLoggerFactory('users-me') |
29 | 41 | ||
@@ -83,6 +95,40 @@ meRouter.post('/me/avatar/pick', | |||
83 | asyncMiddleware(updateMyAvatar) | 95 | asyncMiddleware(updateMyAvatar) |
84 | ) | 96 | ) |
85 | 97 | ||
98 | // ##### Subscriptions part ##### | ||
99 | |||
100 | meRouter.get('/me/subscriptions', | ||
101 | authenticate, | ||
102 | paginationValidator, | ||
103 | userSubscriptionsSortValidator, | ||
104 | setDefaultSort, | ||
105 | setDefaultPagination, | ||
106 | asyncMiddleware(getUserSubscriptions) | ||
107 | ) | ||
108 | |||
109 | meRouter.post('/me/subscriptions', | ||
110 | authenticate, | ||
111 | userSubscriptionAddValidator, | ||
112 | asyncMiddleware(addUserSubscription) | ||
113 | ) | ||
114 | |||
115 | meRouter.delete('/me/subscriptions/:uri', | ||
116 | authenticate, | ||
117 | userSubscriptionRemoveValidator, | ||
118 | asyncMiddleware(deleteUserSubscription) | ||
119 | ) | ||
120 | |||
121 | meRouter.get('/me/subscriptions/videos', | ||
122 | authenticate, | ||
123 | authenticate, | ||
124 | paginationValidator, | ||
125 | videosSortValidator, | ||
126 | setDefaultSort, | ||
127 | setDefaultPagination, | ||
128 | commonVideosFiltersValidator, | ||
129 | asyncMiddleware(getUserSubscriptionVideos) | ||
130 | ) | ||
131 | |||
86 | // --------------------------------------------------------------------------- | 132 | // --------------------------------------------------------------------------- |
87 | 133 | ||
88 | export { | 134 | export { |
@@ -91,6 +137,62 @@ export { | |||
91 | 137 | ||
92 | // --------------------------------------------------------------------------- | 138 | // --------------------------------------------------------------------------- |
93 | 139 | ||
140 | async function addUserSubscription (req: express.Request, res: express.Response) { | ||
141 | const user = res.locals.oauth.token.User as UserModel | ||
142 | const [ name, host ] = req.body.uri.split('@') | ||
143 | |||
144 | const payload = { | ||
145 | name, | ||
146 | host, | ||
147 | followerActorId: user.Account.Actor.id | ||
148 | } | ||
149 | |||
150 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) | ||
151 | .catch(err => logger.error('Cannot create follow job for subscription %s.', req.body.uri, err)) | ||
152 | |||
153 | return res.status(204).end() | ||
154 | } | ||
155 | |||
156 | async function deleteUserSubscription (req: express.Request, res: express.Response) { | ||
157 | const subscription: ActorFollowModel = res.locals.subscription | ||
158 | |||
159 | await sequelizeTypescript.transaction(async t => { | ||
160 | return subscription.destroy({ transaction: t }) | ||
161 | }) | ||
162 | |||
163 | return res.type('json').status(204).end() | ||
164 | } | ||
165 | |||
166 | async function getUserSubscriptions (req: express.Request, res: express.Response) { | ||
167 | const user = res.locals.oauth.token.User as UserModel | ||
168 | const actorId = user.Account.Actor.id | ||
169 | |||
170 | const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort) | ||
171 | |||
172 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
173 | } | ||
174 | |||
175 | async function getUserSubscriptionVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
176 | const user = res.locals.oauth.token.User as UserModel | ||
177 | const resultList = await VideoModel.listForApi({ | ||
178 | start: req.query.start, | ||
179 | count: req.query.count, | ||
180 | sort: req.query.sort, | ||
181 | includeLocalVideos: false, | ||
182 | categoryOneOf: req.query.categoryOneOf, | ||
183 | licenceOneOf: req.query.licenceOneOf, | ||
184 | languageOneOf: req.query.languageOneOf, | ||
185 | tagsOneOf: req.query.tagsOneOf, | ||
186 | tagsAllOf: req.query.tagsAllOf, | ||
187 | nsfw: buildNSFWFilter(res, req.query.nsfw), | ||
188 | filter: req.query.filter as VideoFilter, | ||
189 | withFiles: false, | ||
190 | actorId: user.Account.Actor.id | ||
191 | }) | ||
192 | |||
193 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
194 | } | ||
195 | |||
94 | async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 196 | async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
95 | const user = res.locals.oauth.token.User as UserModel | 197 | const user = res.locals.oauth.token.User as UserModel |
96 | const resultList = await VideoModel.listUserVideosForApi( | 198 | const resultList = await VideoModel.listUserVideosForApi( |
@@ -150,7 +252,7 @@ async function getUserVideoRating (req: express.Request, res: express.Response, | |||
150 | videoId, | 252 | videoId, |
151 | rating | 253 | rating |
152 | } | 254 | } |
153 | res.json(json) | 255 | return res.json(json) |
154 | } | 256 | } |
155 | 257 | ||
156 | async function deleteMe (req: express.Request, res: express.Response) { | 258 | async function deleteMe (req: express.Request, res: express.Response) { |
@@ -207,9 +309,5 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next | |||
207 | oldUserAuditView | 309 | oldUserAuditView |
208 | ) | 310 | ) |
209 | 311 | ||
210 | return res | 312 | return res.json({ avatar: avatar.toFormattedJSON() }) |
211 | .json({ | ||
212 | avatar: avatar.toFormattedJSON() | ||
213 | }) | ||
214 | .end() | ||
215 | } | 313 | } |
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 023ebbedf..6ffc09f87 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -215,6 +215,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon | |||
215 | start: req.query.start, | 215 | start: req.query.start, |
216 | count: req.query.count, | 216 | count: req.query.count, |
217 | sort: req.query.sort, | 217 | sort: req.query.sort, |
218 | includeLocalVideos: false, | ||
218 | categoryOneOf: req.query.categoryOneOf, | 219 | categoryOneOf: req.query.categoryOneOf, |
219 | licenceOneOf: req.query.licenceOneOf, | 220 | licenceOneOf: req.query.licenceOneOf, |
220 | languageOneOf: req.query.languageOneOf, | 221 | languageOneOf: req.query.languageOneOf, |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 92c6ee697..e973aa43f 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -414,6 +414,7 @@ async function listVideos (req: express.Request, res: express.Response, next: ex | |||
414 | start: req.query.start, | 414 | start: req.query.start, |
415 | count: req.query.count, | 415 | count: req.query.count, |
416 | sort: req.query.sort, | 416 | sort: req.query.sort, |
417 | includeLocalVideos: true, | ||
417 | categoryOneOf: req.query.categoryOneOf, | 418 | categoryOneOf: req.query.categoryOneOf, |
418 | licenceOneOf: req.query.licenceOneOf, | 419 | licenceOneOf: req.query.licenceOneOf, |
419 | languageOneOf: req.query.languageOneOf, | 420 | languageOneOf: req.query.languageOneOf, |
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 682f4abda..b30ad8e8d 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -96,6 +96,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n | |||
96 | start, | 96 | start, |
97 | count: FEEDS.COUNT, | 97 | count: FEEDS.COUNT, |
98 | sort: req.query.sort, | 98 | sort: req.query.sort, |
99 | includeLocalVideos: true, | ||
99 | nsfw, | 100 | nsfw, |
100 | filter: req.query.filter, | 101 | filter: req.query.filter, |
101 | withFiles: true, | 102 | withFiles: true, |
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts index c7a64e24d..ae5014f8f 100644 --- a/server/helpers/custom-validators/activitypub/actor.ts +++ b/server/helpers/custom-validators/activitypub/actor.ts | |||
@@ -3,6 +3,7 @@ import { CONSTRAINTS_FIELDS } from '../../../initializers' | |||
3 | import { exists } from '../misc' | 3 | import { exists } from '../misc' |
4 | import { truncate } from 'lodash' | 4 | import { truncate } from 'lodash' |
5 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 5 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
6 | import { isHostValid } from '../servers' | ||
6 | 7 | ||
7 | function isActorEndpointsObjectValid (endpointObject: any) { | 8 | function isActorEndpointsObjectValid (endpointObject: any) { |
8 | return isActivityPubUrlValid(endpointObject.sharedInbox) | 9 | return isActivityPubUrlValid(endpointObject.sharedInbox) |
@@ -109,6 +110,15 @@ function normalizeActor (actor: any) { | |||
109 | return | 110 | return |
110 | } | 111 | } |
111 | 112 | ||
113 | function isValidActorHandle (handle: string) { | ||
114 | if (!exists(handle)) return false | ||
115 | |||
116 | const parts = handle.split('@') | ||
117 | if (parts.length !== 2) return false | ||
118 | |||
119 | return isHostValid(parts[1]) | ||
120 | } | ||
121 | |||
112 | // --------------------------------------------------------------------------- | 122 | // --------------------------------------------------------------------------- |
113 | 123 | ||
114 | export { | 124 | export { |
@@ -126,5 +136,6 @@ export { | |||
126 | isActorAcceptActivityValid, | 136 | isActorAcceptActivityValid, |
127 | isActorRejectActivityValid, | 137 | isActorRejectActivityValid, |
128 | isActorDeleteActivityValid, | 138 | isActorDeleteActivityValid, |
129 | isActorUpdateActivityValid | 139 | isActorUpdateActivityValid, |
140 | isValidActorHandle | ||
130 | } | 141 | } |
diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts index 2a6f56840..32faf36f7 100644 --- a/server/helpers/custom-validators/video-channels.ts +++ b/server/helpers/custom-validators/video-channels.ts | |||
@@ -5,6 +5,7 @@ import * as validator from 'validator' | |||
5 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 5 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
6 | import { VideoChannelModel } from '../../models/video/video-channel' | 6 | import { VideoChannelModel } from '../../models/video/video-channel' |
7 | import { exists } from './misc' | 7 | import { exists } from './misc' |
8 | import { Response } from 'express' | ||
8 | 9 | ||
9 | const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS | 10 | const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS |
10 | 11 | ||
@@ -20,6 +21,12 @@ function isVideoChannelSupportValid (value: string) { | |||
20 | return value === null || (exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.SUPPORT)) | 21 | return value === null || (exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.SUPPORT)) |
21 | } | 22 | } |
22 | 23 | ||
24 | async function isLocalVideoChannelNameExist (name: string, res: Response) { | ||
25 | const videoChannel = await VideoChannelModel.loadLocalByName(name) | ||
26 | |||
27 | return processVideoChannelExist(videoChannel, res) | ||
28 | } | ||
29 | |||
23 | async function isVideoChannelExist (id: string, res: express.Response) { | 30 | async function isVideoChannelExist (id: string, res: express.Response) { |
24 | let videoChannel: VideoChannelModel | 31 | let videoChannel: VideoChannelModel |
25 | if (validator.isInt(id)) { | 32 | if (validator.isInt(id)) { |
@@ -28,23 +35,28 @@ async function isVideoChannelExist (id: string, res: express.Response) { | |||
28 | videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount(id) | 35 | videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount(id) |
29 | } | 36 | } |
30 | 37 | ||
31 | if (!videoChannel) { | 38 | return processVideoChannelExist(videoChannel, res) |
32 | res.status(404) | ||
33 | .json({ error: 'Video channel not found' }) | ||
34 | .end() | ||
35 | |||
36 | return false | ||
37 | } | ||
38 | |||
39 | res.locals.videoChannel = videoChannel | ||
40 | return true | ||
41 | } | 39 | } |
42 | 40 | ||
43 | // --------------------------------------------------------------------------- | 41 | // --------------------------------------------------------------------------- |
44 | 42 | ||
45 | export { | 43 | export { |
44 | isLocalVideoChannelNameExist, | ||
46 | isVideoChannelDescriptionValid, | 45 | isVideoChannelDescriptionValid, |
47 | isVideoChannelNameValid, | 46 | isVideoChannelNameValid, |
48 | isVideoChannelSupportValid, | 47 | isVideoChannelSupportValid, |
49 | isVideoChannelExist | 48 | isVideoChannelExist |
50 | } | 49 | } |
50 | |||
51 | function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) { | ||
52 | if (!videoChannel) { | ||
53 | res.status(404) | ||
54 | .json({ error: 'Video channel not found' }) | ||
55 | .end() | ||
56 | |||
57 | return false | ||
58 | } | ||
59 | |||
60 | res.locals.videoChannel = videoChannel | ||
61 | return true | ||
62 | } | ||
diff --git a/server/helpers/custom-validators/webfinger.ts b/server/helpers/custom-validators/webfinger.ts index d8c1232ce..80a7e4a9d 100644 --- a/server/helpers/custom-validators/webfinger.ts +++ b/server/helpers/custom-validators/webfinger.ts | |||
@@ -2,7 +2,7 @@ import { CONFIG, REMOTE_SCHEME } from '../../initializers' | |||
2 | import { sanitizeHost } from '../core-utils' | 2 | import { sanitizeHost } from '../core-utils' |
3 | import { exists } from './misc' | 3 | import { exists } from './misc' |
4 | 4 | ||
5 | function isWebfingerResourceValid (value: string) { | 5 | function isWebfingerLocalResourceValid (value: string) { |
6 | if (!exists(value)) return false | 6 | if (!exists(value)) return false |
7 | if (value.startsWith('acct:') === false) return false | 7 | if (value.startsWith('acct:') === false) return false |
8 | 8 | ||
@@ -17,5 +17,5 @@ function isWebfingerResourceValid (value: string) { | |||
17 | // --------------------------------------------------------------------------- | 17 | // --------------------------------------------------------------------------- |
18 | 18 | ||
19 | export { | 19 | export { |
20 | isWebfingerResourceValid | 20 | isWebfingerLocalResourceValid |
21 | } | 21 | } |
diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts index 688bf2bab..5c60de10c 100644 --- a/server/helpers/webfinger.ts +++ b/server/helpers/webfinger.ts | |||
@@ -11,15 +11,17 @@ const webfinger = new WebFinger({ | |||
11 | request_timeout: 3000 | 11 | request_timeout: 3000 |
12 | }) | 12 | }) |
13 | 13 | ||
14 | async function loadActorUrlOrGetFromWebfinger (name: string, host: string) { | 14 | async function loadActorUrlOrGetFromWebfinger (uri: string) { |
15 | const [ name, host ] = uri.split('@') | ||
16 | |||
15 | const actor = await ActorModel.loadByNameAndHost(name, host) | 17 | const actor = await ActorModel.loadByNameAndHost(name, host) |
16 | if (actor) return actor.url | 18 | if (actor) return actor.url |
17 | 19 | ||
18 | return getUrlFromWebfinger(name, host) | 20 | return getUrlFromWebfinger(uri) |
19 | } | 21 | } |
20 | 22 | ||
21 | async function getUrlFromWebfinger (name: string, host: string) { | 23 | async function getUrlFromWebfinger (uri: string) { |
22 | const webfingerData: WebFingerData = await webfingerLookup(name + '@' + host) | 24 | const webfingerData: WebFingerData = await webfingerLookup(uri) |
23 | return getLinkOrThrow(webfingerData) | 25 | return getLinkOrThrow(webfingerData) |
24 | } | 26 | } |
25 | 27 | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index bce797159..7e865fe3b 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -31,6 +31,7 @@ const PAGINATION = { | |||
31 | // Sortable columns per schema | 31 | // Sortable columns per schema |
32 | const SORTABLE_COLUMNS = { | 32 | const SORTABLE_COLUMNS = { |
33 | USERS: [ 'id', 'username', 'createdAt' ], | 33 | USERS: [ 'id', 'username', 'createdAt' ], |
34 | USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ], | ||
34 | ACCOUNTS: [ 'createdAt' ], | 35 | ACCOUNTS: [ 'createdAt' ], |
35 | JOBS: [ 'createdAt' ], | 36 | JOBS: [ 'createdAt' ], |
36 | VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ], | 37 | VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ], |
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index b67d9f08b..d84b465b2 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -352,7 +352,7 @@ async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> { | |||
352 | if (!actor.isOutdated()) return actor | 352 | if (!actor.isOutdated()) return actor |
353 | 353 | ||
354 | try { | 354 | try { |
355 | const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost()) | 355 | const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) |
356 | const result = await fetchRemoteActor(actorUrl) | 356 | const result = await fetchRemoteActor(actorUrl) |
357 | if (result === undefined) { | 357 | if (result === undefined) { |
358 | logger.warn('Cannot fetch remote actor in refresh actor.') | 358 | logger.warn('Cannot fetch remote actor in refresh actor.') |
diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts index dfee1ec3e..ef679707b 100644 --- a/server/lib/activitypub/send/send-accept.ts +++ b/server/lib/activitypub/send/send-accept.ts | |||
@@ -10,6 +10,11 @@ async function sendAccept (actorFollow: ActorFollowModel) { | |||
10 | const follower = actorFollow.ActorFollower | 10 | const follower = actorFollow.ActorFollower |
11 | const me = actorFollow.ActorFollowing | 11 | const me = actorFollow.ActorFollowing |
12 | 12 | ||
13 | if (!follower.serverId) { // This should never happen | ||
14 | logger.warn('Do not sending accept to local follower.') | ||
15 | return | ||
16 | } | ||
17 | |||
13 | logger.info('Creating job to accept follower %s.', follower.url) | 18 | logger.info('Creating job to accept follower %s.', follower.url) |
14 | 19 | ||
15 | const followUrl = getActorFollowActivityPubUrl(actorFollow) | 20 | const followUrl = getActorFollowActivityPubUrl(actorFollow) |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index f7a8cf0b3..fc76cdd8a 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -33,6 +33,8 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) { | |||
33 | } | 33 | } |
34 | 34 | ||
35 | async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel, t: Transaction) { | 35 | async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel, t: Transaction) { |
36 | if (!video.VideoChannel.Account.Actor.serverId) return // Local | ||
37 | |||
36 | const url = getVideoAbuseActivityPubUrl(videoAbuse) | 38 | const url = getVideoAbuseActivityPubUrl(videoAbuse) |
37 | 39 | ||
38 | logger.info('Creating job to send video abuse %s.', url) | 40 | logger.info('Creating job to send video abuse %s.', url) |
diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts index 2faffe6e7..46d08c17b 100644 --- a/server/lib/activitypub/send/send-follow.ts +++ b/server/lib/activitypub/send/send-follow.ts | |||
@@ -9,6 +9,9 @@ function sendFollow (actorFollow: ActorFollowModel) { | |||
9 | const me = actorFollow.ActorFollower | 9 | const me = actorFollow.ActorFollower |
10 | const following = actorFollow.ActorFollowing | 10 | const following = actorFollow.ActorFollowing |
11 | 11 | ||
12 | // Same server as ours | ||
13 | if (!following.serverId) return | ||
14 | |||
12 | logger.info('Creating job to send follow request to %s.', following.url) | 15 | logger.info('Creating job to send follow request to %s.', following.url) |
13 | 16 | ||
14 | const url = getActorFollowActivityPubUrl(actorFollow) | 17 | const url = getActorFollowActivityPubUrl(actorFollow) |
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index 4e5dd3973..30d0fd98b 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts | |||
@@ -24,6 +24,9 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { | |||
24 | const me = actorFollow.ActorFollower | 24 | const me = actorFollow.ActorFollower |
25 | const following = actorFollow.ActorFollowing | 25 | const following = actorFollow.ActorFollowing |
26 | 26 | ||
27 | // Same server as ours | ||
28 | if (!following.serverId) return | ||
29 | |||
27 | logger.info('Creating job to send an unfollow request to %s.', following.url) | 30 | logger.info('Creating job to send an unfollow request to %s.', following.url) |
28 | 31 | ||
29 | const followUrl = getActorFollowActivityPubUrl(actorFollow) | 32 | const followUrl = getActorFollowActivityPubUrl(actorFollow) |
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts index 0d28444ec..da437292e 100644 --- a/server/lib/activitypub/send/utils.ts +++ b/server/lib/activitypub/send/utils.ts | |||
@@ -6,6 +6,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | |||
6 | import { JobQueue } from '../../job-queue' | 6 | import { JobQueue } from '../../job-queue' |
7 | import { VideoModel } from '../../../models/video/video' | 7 | import { VideoModel } from '../../../models/video/video' |
8 | import { getActorsInvolvedInVideo } from '../audience' | 8 | import { getActorsInvolvedInVideo } from '../audience' |
9 | import { getServerActor } from '../../../helpers/utils' | ||
9 | 10 | ||
10 | async function forwardVideoRelatedActivity ( | 11 | async function forwardVideoRelatedActivity ( |
11 | activity: Activity, | 12 | activity: Activity, |
@@ -118,14 +119,28 @@ async function computeFollowerUris (toActorFollower: ActorModel[], actorsExcepti | |||
118 | const toActorFollowerIds = toActorFollower.map(a => a.id) | 119 | const toActorFollowerIds = toActorFollower.map(a => a.id) |
119 | 120 | ||
120 | const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) | 121 | const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) |
121 | const sharedInboxesException = actorsException.map(f => f.sharedInboxUrl || f.inboxUrl) | 122 | const sharedInboxesException = await buildSharedInboxesException(actorsException) |
123 | |||
122 | return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) | 124 | return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) |
123 | } | 125 | } |
124 | 126 | ||
125 | async function computeUris (toActors: ActorModel[], actorsException: ActorModel[] = []) { | 127 | async function computeUris (toActors: ActorModel[], actorsException: ActorModel[] = []) { |
126 | const toActorSharedInboxesSet = new Set(toActors.map(a => a.sharedInboxUrl || a.inboxUrl)) | 128 | const serverActor = await getServerActor() |
129 | const targetUrls = toActors | ||
130 | .filter(a => a.id !== serverActor.id) // Don't send to ourselves | ||
131 | .map(a => a.sharedInboxUrl || a.inboxUrl) | ||
132 | |||
133 | const toActorSharedInboxesSet = new Set(targetUrls) | ||
127 | 134 | ||
128 | const sharedInboxesException = actorsException.map(f => f.sharedInboxUrl || f.inboxUrl) | 135 | const sharedInboxesException = await buildSharedInboxesException(actorsException) |
129 | return Array.from(toActorSharedInboxesSet) | 136 | return Array.from(toActorSharedInboxesSet) |
130 | .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) | 137 | .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) |
131 | } | 138 | } |
139 | |||
140 | async function buildSharedInboxesException (actorsException: ActorModel[]) { | ||
141 | const serverActor = await getServerActor() | ||
142 | |||
143 | return actorsException | ||
144 | .map(f => f.sharedInboxUrl || f.inboxUrl) | ||
145 | .concat([ serverActor.sharedInboxUrl ]) | ||
146 | } | ||
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts index 393c6936c..282dde268 100644 --- a/server/lib/job-queue/handlers/activitypub-follow.ts +++ b/server/lib/job-queue/handlers/activitypub-follow.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { logger } from '../../../helpers/logger' | 2 | import { logger } from '../../../helpers/logger' |
3 | import { getServerActor } from '../../../helpers/utils' | 3 | import { getServerActor } from '../../../helpers/utils' |
4 | import { REMOTE_SCHEME, sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers' | 4 | import { REMOTE_SCHEME, sequelizeTypescript } from '../../../initializers' |
5 | import { sendFollow } from '../../activitypub/send' | 5 | import { sendFollow } from '../../activitypub/send' |
6 | import { sanitizeHost } from '../../../helpers/core-utils' | 6 | import { sanitizeHost } from '../../../helpers/core-utils' |
7 | import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger' | 7 | import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger' |
@@ -11,6 +11,8 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | |||
11 | import { ActorModel } from '../../../models/activitypub/actor' | 11 | import { ActorModel } from '../../../models/activitypub/actor' |
12 | 12 | ||
13 | export type ActivitypubFollowPayload = { | 13 | export type ActivitypubFollowPayload = { |
14 | followerActorId: number | ||
15 | name: string | ||
14 | host: string | 16 | host: string |
15 | } | 17 | } |
16 | 18 | ||
@@ -22,10 +24,10 @@ async function processActivityPubFollow (job: Bull.Job) { | |||
22 | 24 | ||
23 | const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) | 25 | const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) |
24 | 26 | ||
25 | const actorUrl = await loadActorUrlOrGetFromWebfinger(SERVER_ACTOR_NAME, sanitizedHost) | 27 | const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost) |
26 | const targetActor = await getOrCreateActorAndServerAndModel(actorUrl) | 28 | const targetActor = await getOrCreateActorAndServerAndModel(actorUrl) |
27 | 29 | ||
28 | const fromActor = await getServerActor() | 30 | const fromActor = await ActorModel.load(payload.followerActorId) |
29 | 31 | ||
30 | return retryTransactionWrapper(follow, fromActor, targetActor) | 32 | return retryTransactionWrapper(follow, fromActor, targetActor) |
31 | } | 33 | } |
@@ -42,6 +44,9 @@ function follow (fromActor: ActorModel, targetActor: ActorModel) { | |||
42 | throw new Error('Follower is the same than target actor.') | 44 | throw new Error('Follower is the same than target actor.') |
43 | } | 45 | } |
44 | 46 | ||
47 | // Same server, direct accept | ||
48 | const state = !fromActor.serverId && !targetActor.serverId ? 'accepted' : 'pending' | ||
49 | |||
45 | return sequelizeTypescript.transaction(async t => { | 50 | return sequelizeTypescript.transaction(async t => { |
46 | const [ actorFollow ] = await ActorFollowModel.findOrCreate({ | 51 | const [ actorFollow ] = await ActorFollowModel.findOrCreate({ |
47 | where: { | 52 | where: { |
@@ -49,7 +54,7 @@ function follow (fromActor: ActorModel, targetActor: ActorModel) { | |||
49 | targetActorId: targetActor.id | 54 | targetActorId: targetActor.id |
50 | }, | 55 | }, |
51 | defaults: { | 56 | defaults: { |
52 | state: 'pending', | 57 | state, |
53 | actorId: fromActor.id, | 58 | actorId: fromActor.id, |
54 | targetActorId: targetActor.id | 59 | targetActorId: targetActor.id |
55 | }, | 60 | }, |
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts index 040ee1f21..faefc1179 100644 --- a/server/middlewares/validators/follows.ts +++ b/server/middlewares/validators/follows.ts | |||
@@ -4,7 +4,7 @@ import { isTestInstance } from '../../helpers/core-utils' | |||
4 | import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers' | 4 | import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers' |
5 | import { logger } from '../../helpers/logger' | 5 | import { logger } from '../../helpers/logger' |
6 | import { getServerActor } from '../../helpers/utils' | 6 | import { getServerActor } from '../../helpers/utils' |
7 | import { CONFIG } from '../../initializers' | 7 | import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers' |
8 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 8 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' |
9 | import { areValidationErrors } from './utils' | 9 | import { areValidationErrors } from './utils' |
10 | 10 | ||
@@ -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.loadByActorAndTargetHost(serverActor.id, req.params.host) | 41 | const follow = await ActorFollowModel.loadByActorAndTargetNameAndHost(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/index.ts b/server/middlewares/validators/index.ts index ccbedd57d..940547a3e 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -6,6 +6,7 @@ export * from './follows' | |||
6 | export * from './feeds' | 6 | export * from './feeds' |
7 | export * from './sort' | 7 | export * from './sort' |
8 | export * from './users' | 8 | export * from './users' |
9 | export * from './user-subscriptions' | ||
9 | export * from './videos' | 10 | export * from './videos' |
10 | export * from './video-abuses' | 11 | export * from './video-abuses' |
11 | export * from './video-blacklist' | 12 | export * from './video-blacklist' |
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index d85611773..b30e97e61 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -14,6 +14,7 @@ const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACK | |||
14 | const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) | 14 | const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) |
15 | const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) | 15 | const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) |
16 | const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) | 16 | const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) |
17 | const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) | ||
17 | 18 | ||
18 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 19 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
19 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 20 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
@@ -27,6 +28,7 @@ const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) | |||
27 | const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) | 28 | const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) |
28 | const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) | 29 | const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) |
29 | const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) | 30 | const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) |
31 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) | ||
30 | 32 | ||
31 | // --------------------------------------------------------------------------- | 33 | // --------------------------------------------------------------------------- |
32 | 34 | ||
@@ -42,5 +44,6 @@ export { | |||
42 | followersSortValidator, | 44 | followersSortValidator, |
43 | followingSortValidator, | 45 | followingSortValidator, |
44 | jobsSortValidator, | 46 | jobsSortValidator, |
45 | videoCommentThreadsSortValidator | 47 | videoCommentThreadsSortValidator, |
48 | userSubscriptionsSortValidator | ||
46 | } | 49 | } |
diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts new file mode 100644 index 000000000..f331b6c34 --- /dev/null +++ b/server/middlewares/validators/user-subscriptions.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'express-validator' | ||
3 | import { body, param } from 'express-validator/check' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { areValidationErrors } from './utils' | ||
6 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | ||
7 | import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' | ||
8 | import { UserModel } from '../../models/account/user' | ||
9 | import { CONFIG } from '../../initializers' | ||
10 | |||
11 | const userSubscriptionAddValidator = [ | ||
12 | body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'), | ||
13 | |||
14 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
15 | logger.debug('Checking userSubscriptionAddValidator parameters', { parameters: req.body }) | ||
16 | |||
17 | if (areValidationErrors(req, res)) return | ||
18 | |||
19 | return next() | ||
20 | } | ||
21 | ] | ||
22 | |||
23 | const userSubscriptionRemoveValidator = [ | ||
24 | param('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to unfollow'), | ||
25 | |||
26 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
27 | logger.debug('Checking unfollow parameters', { parameters: req.params }) | ||
28 | |||
29 | if (areValidationErrors(req, res)) return | ||
30 | |||
31 | let [ name, host ] = req.params.uri.split('@') | ||
32 | if (host === CONFIG.WEBSERVER.HOST) host = null | ||
33 | |||
34 | const user: UserModel = res.locals.oauth.token.User | ||
35 | const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHost(user.Account.Actor.id, name, host) | ||
36 | |||
37 | if (!subscription) { | ||
38 | return res | ||
39 | .status(404) | ||
40 | .json({ | ||
41 | error: `Subscription ${req.params.uri} not found.` | ||
42 | }) | ||
43 | .end() | ||
44 | } | ||
45 | |||
46 | res.locals.subscription = subscription | ||
47 | return next() | ||
48 | } | ||
49 | ] | ||
50 | |||
51 | // --------------------------------------------------------------------------- | ||
52 | |||
53 | export { | ||
54 | userSubscriptionAddValidator, | ||
55 | userSubscriptionRemoveValidator | ||
56 | } | ||
57 | |||
58 | // --------------------------------------------------------------------------- | ||
diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts index 143ce9582..d354c7e05 100644 --- a/server/middlewares/validators/video-channels.ts +++ b/server/middlewares/validators/video-channels.ts | |||
@@ -4,6 +4,7 @@ import { UserRight } from '../../../shared' | |||
4 | import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' | 4 | import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' |
5 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | 5 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' |
6 | import { | 6 | import { |
7 | isLocalVideoChannelNameExist, | ||
7 | isVideoChannelDescriptionValid, | 8 | isVideoChannelDescriptionValid, |
8 | isVideoChannelExist, | 9 | isVideoChannelExist, |
9 | isVideoChannelNameValid, | 10 | isVideoChannelNameValid, |
@@ -100,6 +101,19 @@ const videoChannelsGetValidator = [ | |||
100 | } | 101 | } |
101 | ] | 102 | ] |
102 | 103 | ||
104 | const localVideoChannelValidator = [ | ||
105 | param('name').custom(isVideoChannelNameValid).withMessage('Should have a valid video channel name'), | ||
106 | |||
107 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
108 | logger.debug('Checking localVideoChannelValidator parameters', { parameters: req.params }) | ||
109 | |||
110 | if (areValidationErrors(req, res)) return | ||
111 | if (!await isLocalVideoChannelNameExist(req.params.name, res)) return | ||
112 | |||
113 | return next() | ||
114 | } | ||
115 | ] | ||
116 | |||
103 | // --------------------------------------------------------------------------- | 117 | // --------------------------------------------------------------------------- |
104 | 118 | ||
105 | export { | 119 | export { |
@@ -107,7 +121,8 @@ export { | |||
107 | videoChannelsAddValidator, | 121 | videoChannelsAddValidator, |
108 | videoChannelsUpdateValidator, | 122 | videoChannelsUpdateValidator, |
109 | videoChannelsRemoveValidator, | 123 | videoChannelsRemoveValidator, |
110 | videoChannelsGetValidator | 124 | videoChannelsGetValidator, |
125 | localVideoChannelValidator | ||
111 | } | 126 | } |
112 | 127 | ||
113 | // --------------------------------------------------------------------------- | 128 | // --------------------------------------------------------------------------- |
diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts index 3b9645048..63a1678ec 100644 --- a/server/middlewares/validators/webfinger.ts +++ b/server/middlewares/validators/webfinger.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { query } from 'express-validator/check' | 2 | import { query } from 'express-validator/check' |
3 | import { isWebfingerResourceValid } from '../../helpers/custom-validators/webfinger' | 3 | import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
5 | import { ActorModel } from '../../models/activitypub/actor' | 5 | import { ActorModel } from '../../models/activitypub/actor' |
6 | import { areValidationErrors } from './utils' | 6 | import { areValidationErrors } from './utils' |
7 | import { getHostWithPort } from '../../helpers/express-utils' | 7 | import { getHostWithPort } from '../../helpers/express-utils' |
8 | 8 | ||
9 | const webfingerValidator = [ | 9 | const webfingerValidator = [ |
10 | query('resource').custom(isWebfingerResourceValid).withMessage('Should have a valid webfinger resource'), | 10 | query('resource').custom(isWebfingerLocalResourceValid).withMessage('Should have a valid webfinger resource'), |
11 | 11 | ||
12 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 12 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
13 | logger.debug('Checking webfinger parameters', { parameters: req.query }) | 13 | logger.debug('Checking webfinger parameters', { parameters: req.query }) |
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index adec5e92b..90a8ac43c 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -2,8 +2,21 @@ import * as Bluebird from 'bluebird' | |||
2 | import { values } from 'lodash' | 2 | import { values } from 'lodash' |
3 | import * as Sequelize from 'sequelize' | 3 | import * as Sequelize from 'sequelize' |
4 | import { | 4 | import { |
5 | AfterCreate, AfterDestroy, AfterUpdate, AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model, | 5 | AfterCreate, |
6 | Table, UpdatedAt | 6 | AfterDestroy, |
7 | AfterUpdate, | ||
8 | AllowNull, | ||
9 | BelongsTo, | ||
10 | Column, | ||
11 | CreatedAt, | ||
12 | DataType, | ||
13 | Default, | ||
14 | ForeignKey, | ||
15 | IsInt, | ||
16 | Max, | ||
17 | Model, | ||
18 | Table, | ||
19 | UpdatedAt | ||
7 | } from 'sequelize-typescript' | 20 | } from 'sequelize-typescript' |
8 | import { FollowState } from '../../../shared/models/actors' | 21 | import { FollowState } from '../../../shared/models/actors' |
9 | import { AccountFollow } from '../../../shared/models/actors/follow.model' | 22 | import { AccountFollow } from '../../../shared/models/actors/follow.model' |
@@ -14,6 +27,7 @@ import { FOLLOW_STATES } from '../../initializers/constants' | |||
14 | import { ServerModel } from '../server/server' | 27 | import { ServerModel } from '../server/server' |
15 | import { getSort } from '../utils' | 28 | import { getSort } from '../utils' |
16 | import { ActorModel } from './actor' | 29 | import { ActorModel } from './actor' |
30 | import { VideoChannelModel } from '../video/video-channel' | ||
17 | 31 | ||
18 | @Table({ | 32 | @Table({ |
19 | tableName: 'actorFollow', | 33 | tableName: 'actorFollow', |
@@ -151,7 +165,32 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
151 | return ActorFollowModel.findOne(query) | 165 | return ActorFollowModel.findOne(query) |
152 | } | 166 | } |
153 | 167 | ||
154 | static loadByActorAndTargetHost (actorId: number, targetHost: string, t?: Sequelize.Transaction) { | 168 | static loadByActorAndTargetNameAndHost (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) { |
169 | const actorFollowingPartInclude = { | ||
170 | model: ActorModel, | ||
171 | required: true, | ||
172 | as: 'ActorFollowing', | ||
173 | where: { | ||
174 | preferredUsername: targetName | ||
175 | } | ||
176 | } | ||
177 | |||
178 | if (targetHost === null) { | ||
179 | actorFollowingPartInclude.where['serverId'] = null | ||
180 | } else { | ||
181 | Object.assign(actorFollowingPartInclude, { | ||
182 | include: [ | ||
183 | { | ||
184 | model: ServerModel, | ||
185 | required: true, | ||
186 | where: { | ||
187 | host: targetHost | ||
188 | } | ||
189 | } | ||
190 | ] | ||
191 | }) | ||
192 | } | ||
193 | |||
155 | const query = { | 194 | const query = { |
156 | where: { | 195 | where: { |
157 | actorId | 196 | actorId |
@@ -162,20 +201,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
162 | required: true, | 201 | required: true, |
163 | as: 'ActorFollower' | 202 | as: 'ActorFollower' |
164 | }, | 203 | }, |
165 | { | 204 | actorFollowingPartInclude |
166 | model: ActorModel, | ||
167 | required: true, | ||
168 | as: 'ActorFollowing', | ||
169 | include: [ | ||
170 | { | ||
171 | model: ServerModel, | ||
172 | required: true, | ||
173 | where: { | ||
174 | host: targetHost | ||
175 | } | ||
176 | } | ||
177 | ] | ||
178 | } | ||
179 | ], | 205 | ], |
180 | transaction: t | 206 | transaction: t |
181 | } | 207 | } |
@@ -216,6 +242,39 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
216 | }) | 242 | }) |
217 | } | 243 | } |
218 | 244 | ||
245 | static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { | ||
246 | const query = { | ||
247 | distinct: true, | ||
248 | offset: start, | ||
249 | limit: count, | ||
250 | order: getSort(sort), | ||
251 | where: { | ||
252 | actorId: id | ||
253 | }, | ||
254 | include: [ | ||
255 | { | ||
256 | model: ActorModel, | ||
257 | as: 'ActorFollowing', | ||
258 | required: true, | ||
259 | include: [ | ||
260 | { | ||
261 | model: VideoChannelModel, | ||
262 | required: true | ||
263 | } | ||
264 | ] | ||
265 | } | ||
266 | ] | ||
267 | } | ||
268 | |||
269 | return ActorFollowModel.findAndCountAll(query) | ||
270 | .then(({ rows, count }) => { | ||
271 | return { | ||
272 | data: rows.map(r => r.ActorFollowing.VideoChannel), | ||
273 | total: count | ||
274 | } | ||
275 | }) | ||
276 | } | ||
277 | |||
219 | static listFollowersForApi (id: number, start: number, count: number, sort: string) { | 278 | static listFollowersForApi (id: number, start: number, count: number, sort: string) { |
220 | const query = { | 279 | const query = { |
221 | distinct: true, | 280 | distinct: true, |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index d0dba18d5..0273fab13 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -1,14 +1,27 @@ | |||
1 | import { | 1 | import { |
2 | AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, HasMany, Is, Model, Scopes, Table, | 2 | AllowNull, |
3 | UpdatedAt, Default, DataType | 3 | BeforeDestroy, |
4 | BelongsTo, | ||
5 | Column, | ||
6 | CreatedAt, | ||
7 | DataType, | ||
8 | Default, | ||
9 | DefaultScope, | ||
10 | ForeignKey, | ||
11 | HasMany, | ||
12 | Is, | ||
13 | Model, | ||
14 | Scopes, | ||
15 | Table, | ||
16 | UpdatedAt | ||
4 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
5 | import { ActivityPubActor } from '../../../shared/models/activitypub' | 18 | import { ActivityPubActor } from '../../../shared/models/activitypub' |
6 | import { VideoChannel } from '../../../shared/models/videos' | 19 | import { VideoChannel } from '../../../shared/models/videos' |
7 | import { | 20 | import { |
8 | isVideoChannelDescriptionValid, isVideoChannelNameValid, | 21 | isVideoChannelDescriptionValid, |
22 | isVideoChannelNameValid, | ||
9 | isVideoChannelSupportValid | 23 | isVideoChannelSupportValid |
10 | } from '../../helpers/custom-validators/video-channels' | 24 | } from '../../helpers/custom-validators/video-channels' |
11 | import { logger } from '../../helpers/logger' | ||
12 | import { sendDeleteActor } from '../../lib/activitypub/send' | 25 | import { sendDeleteActor } from '../../lib/activitypub/send' |
13 | import { AccountModel } from '../account/account' | 26 | import { AccountModel } from '../account/account' |
14 | import { ActorModel } from '../activitypub/actor' | 27 | import { ActorModel } from '../activitypub/actor' |
@@ -241,6 +254,23 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
241 | .findById(id, options) | 254 | .findById(id, options) |
242 | } | 255 | } |
243 | 256 | ||
257 | static loadLocalByName (name: string) { | ||
258 | const query = { | ||
259 | include: [ | ||
260 | { | ||
261 | model: ActorModel, | ||
262 | required: true, | ||
263 | where: { | ||
264 | preferredUsername: name, | ||
265 | serverId: null | ||
266 | } | ||
267 | } | ||
268 | ] | ||
269 | } | ||
270 | |||
271 | return VideoChannelModel.findOne(query) | ||
272 | } | ||
273 | |||
244 | toFormattedJSON (): VideoChannel { | 274 | toFormattedJSON (): VideoChannel { |
245 | const actor = this.Actor.toFormattedJSON() | 275 | const actor = this.Actor.toFormattedJSON() |
246 | const videoChannel = { | 276 | const videoChannel = { |
@@ -251,8 +281,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
251 | isLocal: this.Actor.isOwned(), | 281 | isLocal: this.Actor.isOwned(), |
252 | createdAt: this.createdAt, | 282 | createdAt: this.createdAt, |
253 | updatedAt: this.updatedAt, | 283 | updatedAt: this.updatedAt, |
254 | ownerAccount: undefined, | 284 | ownerAccount: undefined |
255 | videos: undefined | ||
256 | } | 285 | } |
257 | 286 | ||
258 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() | 287 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index b13dee403..5db718061 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -133,6 +133,7 @@ export enum ScopeNames { | |||
133 | 133 | ||
134 | type AvailableForListOptions = { | 134 | type AvailableForListOptions = { |
135 | actorId: number, | 135 | actorId: number, |
136 | includeLocalVideos: boolean, | ||
136 | filter?: VideoFilter, | 137 | filter?: VideoFilter, |
137 | categoryOneOf?: number[], | 138 | categoryOneOf?: number[], |
138 | nsfw?: boolean, | 139 | nsfw?: boolean, |
@@ -201,6 +202,15 @@ type AvailableForListOptions = { | |||
201 | 202 | ||
202 | // Force actorId to be a number to avoid SQL injections | 203 | // Force actorId to be a number to avoid SQL injections |
203 | const actorIdNumber = parseInt(options.actorId.toString(), 10) | 204 | const actorIdNumber = parseInt(options.actorId.toString(), 10) |
205 | let localVideosReq = '' | ||
206 | if (options.includeLocalVideos === true) { | ||
207 | localVideosReq = ' UNION ALL ' + | ||
208 | 'SELECT "video"."id" AS "id" FROM "video" ' + | ||
209 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
210 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + | ||
211 | 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + | ||
212 | 'WHERE "actor"."serverId" IS NULL' | ||
213 | } | ||
204 | 214 | ||
205 | // FIXME: It would be more efficient to use a CTE so we join AFTER the filters, but sequelize does not support it... | 215 | // FIXME: It would be more efficient to use a CTE so we join AFTER the filters, but sequelize does not support it... |
206 | const query: IFindOptions<VideoModel> = { | 216 | const query: IFindOptions<VideoModel> = { |
@@ -214,12 +224,6 @@ type AvailableForListOptions = { | |||
214 | 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + | 224 | 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + |
215 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + | 225 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + |
216 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | 226 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + |
217 | ' UNION ' + | ||
218 | 'SELECT "video"."id" AS "id" FROM "video" ' + | ||
219 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
220 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + | ||
221 | 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + | ||
222 | 'WHERE "actor"."serverId" IS NULL ' + | ||
223 | ' UNION ALL ' + | 227 | ' UNION ALL ' + |
224 | 'SELECT "video"."id" AS "id" FROM "video" ' + | 228 | 'SELECT "video"."id" AS "id" FROM "video" ' + |
225 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | 229 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + |
@@ -227,6 +231,7 @@ type AvailableForListOptions = { | |||
227 | 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + | 231 | 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + |
228 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' + | 232 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' + |
229 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | 233 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + |
234 | localVideosReq + | ||
230 | ')' | 235 | ')' |
231 | ) | 236 | ) |
232 | }, | 237 | }, |
@@ -825,6 +830,7 @@ export class VideoModel extends Model<VideoModel> { | |||
825 | count: number, | 830 | count: number, |
826 | sort: string, | 831 | sort: string, |
827 | nsfw: boolean, | 832 | nsfw: boolean, |
833 | includeLocalVideos: boolean, | ||
828 | withFiles: boolean, | 834 | withFiles: boolean, |
829 | categoryOneOf?: number[], | 835 | categoryOneOf?: number[], |
830 | licenceOneOf?: number[], | 836 | licenceOneOf?: number[], |
@@ -833,7 +839,8 @@ export class VideoModel extends Model<VideoModel> { | |||
833 | tagsAllOf?: string[], | 839 | tagsAllOf?: string[], |
834 | filter?: VideoFilter, | 840 | filter?: VideoFilter, |
835 | accountId?: number, | 841 | accountId?: number, |
836 | videoChannelId?: number | 842 | videoChannelId?: number, |
843 | actorId?: number | ||
837 | }) { | 844 | }) { |
838 | const query = { | 845 | const query = { |
839 | offset: options.start, | 846 | offset: options.start, |
@@ -841,11 +848,12 @@ export class VideoModel extends Model<VideoModel> { | |||
841 | order: getSort(options.sort) | 848 | order: getSort(options.sort) |
842 | } | 849 | } |
843 | 850 | ||
844 | const serverActor = await getServerActor() | 851 | const actorId = options.actorId || (await getServerActor()).id |
852 | |||
845 | const scopes = { | 853 | const scopes = { |
846 | method: [ | 854 | method: [ |
847 | ScopeNames.AVAILABLE_FOR_LIST, { | 855 | ScopeNames.AVAILABLE_FOR_LIST, { |
848 | actorId: serverActor.id, | 856 | actorId, |
849 | nsfw: options.nsfw, | 857 | nsfw: options.nsfw, |
850 | categoryOneOf: options.categoryOneOf, | 858 | categoryOneOf: options.categoryOneOf, |
851 | licenceOneOf: options.licenceOneOf, | 859 | licenceOneOf: options.licenceOneOf, |
@@ -855,7 +863,8 @@ export class VideoModel extends Model<VideoModel> { | |||
855 | filter: options.filter, | 863 | filter: options.filter, |
856 | withFiles: options.withFiles, | 864 | withFiles: options.withFiles, |
857 | accountId: options.accountId, | 865 | accountId: options.accountId, |
858 | videoChannelId: options.videoChannelId | 866 | videoChannelId: options.videoChannelId, |
867 | includeLocalVideos: options.includeLocalVideos | ||
859 | } as AvailableForListOptions | 868 | } as AvailableForListOptions |
860 | ] | 869 | ] |
861 | } | 870 | } |
@@ -871,6 +880,7 @@ export class VideoModel extends Model<VideoModel> { | |||
871 | } | 880 | } |
872 | 881 | ||
873 | static async searchAndPopulateAccountAndServer (options: { | 882 | static async searchAndPopulateAccountAndServer (options: { |
883 | includeLocalVideos: boolean | ||
874 | search?: string | 884 | search?: string |
875 | start?: number | 885 | start?: number |
876 | count?: number | 886 | count?: number |
@@ -955,6 +965,7 @@ export class VideoModel extends Model<VideoModel> { | |||
955 | method: [ | 965 | method: [ |
956 | ScopeNames.AVAILABLE_FOR_LIST, { | 966 | ScopeNames.AVAILABLE_FOR_LIST, { |
957 | actorId: serverActor.id, | 967 | actorId: serverActor.id, |
968 | includeLocalVideos: options.includeLocalVideos, | ||
958 | nsfw: options.nsfw, | 969 | nsfw: options.nsfw, |
959 | categoryOneOf: options.categoryOneOf, | 970 | categoryOneOf: options.categoryOneOf, |
960 | licenceOneOf: options.licenceOneOf, | 971 | licenceOneOf: options.licenceOneOf, |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 03fdd5c4e..777acbb0f 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -12,3 +12,4 @@ import './video-comments' | |||
12 | import './videos' | 12 | import './videos' |
13 | import './video-imports' | 13 | import './video-imports' |
14 | import './search' | 14 | import './search' |
15 | import './user-subscriptions' | ||
diff --git a/server/tests/api/check-params/user-subscriptions.ts b/server/tests/api/check-params/user-subscriptions.ts new file mode 100644 index 000000000..9f7d15b27 --- /dev/null +++ b/server/tests/api/check-params/user-subscriptions.ts | |||
@@ -0,0 +1,220 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | |||
5 | import { | ||
6 | createUser, | ||
7 | flushTests, | ||
8 | getMyUserInformation, | ||
9 | killallServers, | ||
10 | makeDeleteRequest, | ||
11 | makeGetRequest, | ||
12 | makePostBodyRequest, | ||
13 | runServer, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, | ||
16 | userLogin | ||
17 | } from '../../utils' | ||
18 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | ||
19 | |||
20 | describe('Test user subscriptions API validators', function () { | ||
21 | const path = '/api/v1/users/me/subscriptions' | ||
22 | let server: ServerInfo | ||
23 | let userAccessToken = '' | ||
24 | let userChannelUUID: string | ||
25 | |||
26 | // --------------------------------------------------------------- | ||
27 | |||
28 | before(async function () { | ||
29 | this.timeout(30000) | ||
30 | |||
31 | await flushTests() | ||
32 | |||
33 | server = await runServer(1) | ||
34 | |||
35 | await setAccessTokensToServers([ server ]) | ||
36 | |||
37 | const user = { | ||
38 | username: 'user1', | ||
39 | password: 'my super password' | ||
40 | } | ||
41 | await createUser(server.url, server.accessToken, user.username, user.password) | ||
42 | userAccessToken = await userLogin(server, user) | ||
43 | |||
44 | { | ||
45 | const res = await getMyUserInformation(server.url, server.accessToken) | ||
46 | userChannelUUID = res.body.videoChannels[ 0 ].uuid | ||
47 | } | ||
48 | }) | ||
49 | |||
50 | describe('When listing my subscriptions', function () { | ||
51 | it('Should fail with a bad start pagination', async function () { | ||
52 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
53 | }) | ||
54 | |||
55 | it('Should fail with a bad count pagination', async function () { | ||
56 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
57 | }) | ||
58 | |||
59 | it('Should fail with an incorrect sort', async function () { | ||
60 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
61 | }) | ||
62 | |||
63 | it('Should fail with a non authenticated user', async function () { | ||
64 | await makeGetRequest({ | ||
65 | url: server.url, | ||
66 | path, | ||
67 | statusCodeExpected: 401 | ||
68 | }) | ||
69 | }) | ||
70 | |||
71 | it('Should success with the correct parameters', async function () { | ||
72 | await await makeGetRequest({ | ||
73 | url: server.url, | ||
74 | path, | ||
75 | token: userAccessToken, | ||
76 | statusCodeExpected: 200 | ||
77 | }) | ||
78 | }) | ||
79 | }) | ||
80 | |||
81 | describe('When listing my subscriptions videos', function () { | ||
82 | const path = '/api/v1/users/me/subscriptions/videos' | ||
83 | |||
84 | it('Should fail with a bad start pagination', async function () { | ||
85 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
86 | }) | ||
87 | |||
88 | it('Should fail with a bad count pagination', async function () { | ||
89 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
90 | }) | ||
91 | |||
92 | it('Should fail with an incorrect sort', async function () { | ||
93 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
94 | }) | ||
95 | |||
96 | it('Should fail with a non authenticated user', async function () { | ||
97 | await makeGetRequest({ | ||
98 | url: server.url, | ||
99 | path, | ||
100 | statusCodeExpected: 401 | ||
101 | }) | ||
102 | }) | ||
103 | |||
104 | it('Should success with the correct parameters', async function () { | ||
105 | await await makeGetRequest({ | ||
106 | url: server.url, | ||
107 | path, | ||
108 | token: userAccessToken, | ||
109 | statusCodeExpected: 200 | ||
110 | }) | ||
111 | }) | ||
112 | }) | ||
113 | |||
114 | describe('When adding a subscription', function () { | ||
115 | it('Should fail with a non authenticated user', async function () { | ||
116 | await makePostBodyRequest({ | ||
117 | url: server.url, | ||
118 | path, | ||
119 | fields: { uri: userChannelUUID + '@localhost:9001' }, | ||
120 | statusCodeExpected: 401 | ||
121 | }) | ||
122 | }) | ||
123 | |||
124 | it('Should fail with bad URIs', async function () { | ||
125 | await makePostBodyRequest({ | ||
126 | url: server.url, | ||
127 | path, | ||
128 | token: server.accessToken, | ||
129 | fields: { uri: 'root' }, | ||
130 | statusCodeExpected: 400 | ||
131 | }) | ||
132 | |||
133 | await makePostBodyRequest({ | ||
134 | url: server.url, | ||
135 | path, | ||
136 | token: server.accessToken, | ||
137 | fields: { uri: 'root@' }, | ||
138 | statusCodeExpected: 400 | ||
139 | }) | ||
140 | |||
141 | await makePostBodyRequest({ | ||
142 | url: server.url, | ||
143 | path, | ||
144 | token: server.accessToken, | ||
145 | fields: { uri: 'root@hello@' }, | ||
146 | statusCodeExpected: 400 | ||
147 | }) | ||
148 | }) | ||
149 | |||
150 | it('Should success with the correct parameters', async function () { | ||
151 | await makePostBodyRequest({ | ||
152 | url: server.url, | ||
153 | path, | ||
154 | token: server.accessToken, | ||
155 | fields: { uri: userChannelUUID + '@localhost:9001' }, | ||
156 | statusCodeExpected: 204 | ||
157 | }) | ||
158 | }) | ||
159 | }) | ||
160 | |||
161 | describe('When removing a subscription', function () { | ||
162 | it('Should fail with a non authenticated user', async function () { | ||
163 | await makeDeleteRequest({ | ||
164 | url: server.url, | ||
165 | path: path + '/' + userChannelUUID + '@localhost:9001', | ||
166 | statusCodeExpected: 401 | ||
167 | }) | ||
168 | }) | ||
169 | |||
170 | it('Should fail with bad URIs', async function () { | ||
171 | await makeDeleteRequest({ | ||
172 | url: server.url, | ||
173 | path: path + '/root', | ||
174 | token: server.accessToken, | ||
175 | statusCodeExpected: 400 | ||
176 | }) | ||
177 | |||
178 | await makeDeleteRequest({ | ||
179 | url: server.url, | ||
180 | path: path + '/root@', | ||
181 | token: server.accessToken, | ||
182 | statusCodeExpected: 400 | ||
183 | }) | ||
184 | |||
185 | await makeDeleteRequest({ | ||
186 | url: server.url, | ||
187 | path: path + '/root@hello@', | ||
188 | token: server.accessToken, | ||
189 | statusCodeExpected: 400 | ||
190 | }) | ||
191 | }) | ||
192 | |||
193 | it('Should fail with an unknown subscription', async function () { | ||
194 | await makeDeleteRequest({ | ||
195 | url: server.url, | ||
196 | path: path + '/root1@localhost:9001', | ||
197 | token: server.accessToken, | ||
198 | statusCodeExpected: 404 | ||
199 | }) | ||
200 | }) | ||
201 | |||
202 | it('Should success with the correct parameters', async function () { | ||
203 | await makeDeleteRequest({ | ||
204 | url: server.url, | ||
205 | path: path + '/' + userChannelUUID + '@localhost:9001', | ||
206 | token: server.accessToken, | ||
207 | statusCodeExpected: 204 | ||
208 | }) | ||
209 | }) | ||
210 | }) | ||
211 | |||
212 | after(async function () { | ||
213 | killallServers([ server ]) | ||
214 | |||
215 | // Keep the logs if the test failed | ||
216 | if (this['ok']) { | ||
217 | await flushTests() | ||
218 | } | ||
219 | }) | ||
220 | }) | ||
diff --git a/server/tests/api/index-slow.ts b/server/tests/api/index-slow.ts index 243c941cb..e24a7b664 100644 --- a/server/tests/api/index-slow.ts +++ b/server/tests/api/index-slow.ts | |||
@@ -6,6 +6,7 @@ import './server/follows' | |||
6 | import './server/jobs' | 6 | import './server/jobs' |
7 | import './videos/video-comments' | 7 | import './videos/video-comments' |
8 | import './users/users-multiple-servers' | 8 | import './users/users-multiple-servers' |
9 | import './users/user-subscriptions' | ||
9 | import './server/handle-down' | 10 | import './server/handle-down' |
10 | import './videos/video-schedule-update' | 11 | import './videos/video-schedule-update' |
11 | import './videos/video-imports' | 12 | import './videos/video-imports' |
diff --git a/server/tests/api/users/user-subscriptions.ts b/server/tests/api/users/user-subscriptions.ts new file mode 100644 index 000000000..2ba6cdfaf --- /dev/null +++ b/server/tests/api/users/user-subscriptions.ts | |||
@@ -0,0 +1,312 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { createUser, doubleFollow, flushAndRunMultipleServers, follow, getVideosList, unfollow, userLogin } from '../../utils' | ||
6 | import { getMyUserInformation, killallServers, ServerInfo, uploadVideo } from '../../utils/index' | ||
7 | import { setAccessTokensToServers } from '../../utils/users/login' | ||
8 | import { Video, VideoChannel } from '../../../../shared/models/videos' | ||
9 | import { waitJobs } from '../../utils/server/jobs' | ||
10 | import { | ||
11 | addUserSubscription, | ||
12 | listUserSubscriptions, | ||
13 | listUserSubscriptionVideos, | ||
14 | removeUserSubscription | ||
15 | } from '../../utils/users/user-subscriptions' | ||
16 | |||
17 | const expect = chai.expect | ||
18 | |||
19 | describe('Test users subscriptions', function () { | ||
20 | let servers: ServerInfo[] = [] | ||
21 | const users: { accessToken: string, videoChannelName: string }[] = [] | ||
22 | let rootChannelNameServer1: string | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(120000) | ||
26 | |||
27 | servers = await flushAndRunMultipleServers(3) | ||
28 | |||
29 | // Get the access tokens | ||
30 | await setAccessTokensToServers(servers) | ||
31 | |||
32 | // Server 1 and server 2 follow each other | ||
33 | await doubleFollow(servers[0], servers[1]) | ||
34 | |||
35 | const res = await getMyUserInformation(servers[0].url, servers[0].accessToken) | ||
36 | rootChannelNameServer1 = res.body.videoChannels[0].name | ||
37 | |||
38 | { | ||
39 | for (const server of servers) { | ||
40 | const user = { username: 'user' + server.serverNumber, password: 'password' } | ||
41 | await createUser(server.url, server.accessToken, user.username, user.password) | ||
42 | |||
43 | const accessToken = await userLogin(server, user) | ||
44 | const res = await getMyUserInformation(server.url, accessToken) | ||
45 | const videoChannels: VideoChannel[] = res.body.videoChannels | ||
46 | |||
47 | users.push({ accessToken, videoChannelName: videoChannels[0].name }) | ||
48 | |||
49 | const videoName1 = 'video 1-' + server.serverNumber | ||
50 | await uploadVideo(server.url, accessToken, { name: videoName1 }) | ||
51 | |||
52 | const videoName2 = 'video 2-' + server.serverNumber | ||
53 | await uploadVideo(server.url, accessToken, { name: videoName2 }) | ||
54 | } | ||
55 | } | ||
56 | |||
57 | await waitJobs(servers) | ||
58 | }) | ||
59 | |||
60 | it('Should display videos of server 2 on server 1', async function () { | ||
61 | const res = await getVideosList(servers[0].url) | ||
62 | |||
63 | expect(res.body.total).to.equal(4) | ||
64 | }) | ||
65 | |||
66 | it('User of server 1 should follow user of server 3 and root of server 1', async function () { | ||
67 | this.timeout(30000) | ||
68 | |||
69 | await addUserSubscription(servers[0].url, users[0].accessToken, users[2].videoChannelName + '@localhost:9003') | ||
70 | await addUserSubscription(servers[0].url, users[0].accessToken, rootChannelNameServer1 + '@localhost:9001') | ||
71 | |||
72 | await waitJobs(servers) | ||
73 | |||
74 | await uploadVideo(servers[2].url, users[2].accessToken, { name: 'video server 3 added after follow' }) | ||
75 | |||
76 | await waitJobs(servers) | ||
77 | }) | ||
78 | |||
79 | it('Should not display videos of server 3 on server 1', async function () { | ||
80 | const res = await getVideosList(servers[0].url) | ||
81 | |||
82 | expect(res.body.total).to.equal(4) | ||
83 | for (const video of res.body.data) { | ||
84 | expect(video.name).to.not.contain('1-3') | ||
85 | expect(video.name).to.not.contain('2-3') | ||
86 | expect(video.name).to.not.contain('video server 3 added after follow') | ||
87 | } | ||
88 | }) | ||
89 | |||
90 | it('Should list subscriptions', async function () { | ||
91 | { | ||
92 | const res = await listUserSubscriptions(servers[0].url, servers[0].accessToken) | ||
93 | expect(res.body.total).to.equal(0) | ||
94 | expect(res.body.data).to.be.an('array') | ||
95 | expect(res.body.data).to.have.lengthOf(0) | ||
96 | } | ||
97 | |||
98 | { | ||
99 | const res = await listUserSubscriptions(servers[0].url, users[0].accessToken) | ||
100 | expect(res.body.total).to.equal(2) | ||
101 | |||
102 | const subscriptions: VideoChannel[] = res.body.data | ||
103 | expect(subscriptions).to.be.an('array') | ||
104 | expect(subscriptions).to.have.lengthOf(2) | ||
105 | |||
106 | expect(subscriptions[0].name).to.equal(users[2].videoChannelName) | ||
107 | expect(subscriptions[1].name).to.equal(rootChannelNameServer1) | ||
108 | } | ||
109 | }) | ||
110 | |||
111 | it('Should list subscription videos', async function () { | ||
112 | { | ||
113 | const res = await listUserSubscriptionVideos(servers[0].url, servers[0].accessToken) | ||
114 | expect(res.body.total).to.equal(0) | ||
115 | expect(res.body.data).to.be.an('array') | ||
116 | expect(res.body.data).to.have.lengthOf(0) | ||
117 | } | ||
118 | |||
119 | { | ||
120 | const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt') | ||
121 | expect(res.body.total).to.equal(3) | ||
122 | |||
123 | const videos: Video[] = res.body.data | ||
124 | expect(videos).to.be.an('array') | ||
125 | expect(videos).to.have.lengthOf(3) | ||
126 | |||
127 | expect(videos[0].name).to.equal('video 1-3') | ||
128 | expect(videos[1].name).to.equal('video 2-3') | ||
129 | expect(videos[2].name).to.equal('video server 3 added after follow') | ||
130 | } | ||
131 | }) | ||
132 | |||
133 | it('Should upload a video by root on server 1 and see it in the subscription videos', async function () { | ||
134 | this.timeout(30000) | ||
135 | |||
136 | const videoName = 'video server 1 added after follow' | ||
137 | await uploadVideo(servers[0].url, servers[0].accessToken, { name: videoName }) | ||
138 | |||
139 | await waitJobs(servers) | ||
140 | |||
141 | { | ||
142 | const res = await listUserSubscriptionVideos(servers[0].url, servers[0].accessToken) | ||
143 | expect(res.body.total).to.equal(0) | ||
144 | expect(res.body.data).to.be.an('array') | ||
145 | expect(res.body.data).to.have.lengthOf(0) | ||
146 | } | ||
147 | |||
148 | { | ||
149 | const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt') | ||
150 | expect(res.body.total).to.equal(4) | ||
151 | |||
152 | const videos: Video[] = res.body.data | ||
153 | expect(videos).to.be.an('array') | ||
154 | expect(videos).to.have.lengthOf(4) | ||
155 | |||
156 | expect(videos[0].name).to.equal('video 1-3') | ||
157 | expect(videos[1].name).to.equal('video 2-3') | ||
158 | expect(videos[2].name).to.equal('video server 3 added after follow') | ||
159 | expect(videos[3].name).to.equal('video server 1 added after follow') | ||
160 | } | ||
161 | |||
162 | { | ||
163 | const res = await getVideosList(servers[0].url) | ||
164 | |||
165 | expect(res.body.total).to.equal(5) | ||
166 | for (const video of res.body.data) { | ||
167 | expect(video.name).to.not.contain('1-3') | ||
168 | expect(video.name).to.not.contain('2-3') | ||
169 | expect(video.name).to.not.contain('video server 3 added after follow') | ||
170 | } | ||
171 | } | ||
172 | }) | ||
173 | |||
174 | it('Should have server 1 follow server 3 and display server 3 videos', async function () { | ||
175 | this.timeout(30000) | ||
176 | |||
177 | await follow(servers[0].url, [ servers[2].url ], servers[0].accessToken) | ||
178 | |||
179 | await waitJobs(servers) | ||
180 | |||
181 | const res = await getVideosList(servers[0].url) | ||
182 | |||
183 | expect(res.body.total).to.equal(8) | ||
184 | |||
185 | const names = [ '1-3', '2-3', 'video server 3 added after follow' ] | ||
186 | for (const name of names) { | ||
187 | const video = res.body.data.find(v => v.name.indexOf(name) === -1) | ||
188 | expect(video).to.not.be.undefined | ||
189 | } | ||
190 | }) | ||
191 | |||
192 | it('Should remove follow server 1 -> server 3 and hide server 3 videos', async function () { | ||
193 | this.timeout(30000) | ||
194 | |||
195 | await unfollow(servers[0].url, servers[0].accessToken, servers[2]) | ||
196 | |||
197 | await waitJobs(servers) | ||
198 | |||
199 | const res = await getVideosList(servers[0].url) | ||
200 | |||
201 | expect(res.body.total).to.equal(5) | ||
202 | for (const video of res.body.data) { | ||
203 | expect(video.name).to.not.contain('1-3') | ||
204 | expect(video.name).to.not.contain('2-3') | ||
205 | expect(video.name).to.not.contain('video server 3 added after follow') | ||
206 | } | ||
207 | }) | ||
208 | |||
209 | it('Should still list subscription videos', async function () { | ||
210 | { | ||
211 | const res = await listUserSubscriptionVideos(servers[0].url, servers[0].accessToken) | ||
212 | expect(res.body.total).to.equal(0) | ||
213 | expect(res.body.data).to.be.an('array') | ||
214 | expect(res.body.data).to.have.lengthOf(0) | ||
215 | } | ||
216 | |||
217 | { | ||
218 | const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt') | ||
219 | expect(res.body.total).to.equal(4) | ||
220 | |||
221 | const videos: Video[] = res.body.data | ||
222 | expect(videos).to.be.an('array') | ||
223 | expect(videos).to.have.lengthOf(4) | ||
224 | |||
225 | expect(videos[0].name).to.equal('video 1-3') | ||
226 | expect(videos[1].name).to.equal('video 2-3') | ||
227 | expect(videos[2].name).to.equal('video server 3 added after follow') | ||
228 | expect(videos[3].name).to.equal('video server 1 added after follow') | ||
229 | } | ||
230 | }) | ||
231 | |||
232 | it('Should remove user of server 3 subscription', async function () { | ||
233 | await removeUserSubscription(servers[0].url, users[0].accessToken, users[2].videoChannelName + '@localhost:9003') | ||
234 | |||
235 | await waitJobs(servers) | ||
236 | }) | ||
237 | |||
238 | it('Should not display its videos anymore', async function () { | ||
239 | { | ||
240 | const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt') | ||
241 | expect(res.body.total).to.equal(1) | ||
242 | |||
243 | const videos: Video[] = res.body.data | ||
244 | expect(videos).to.be.an('array') | ||
245 | expect(videos).to.have.lengthOf(1) | ||
246 | |||
247 | expect(videos[0].name).to.equal('video server 1 added after follow') | ||
248 | } | ||
249 | }) | ||
250 | |||
251 | it('Should remove the root subscription and not display the videos anymore', async function () { | ||
252 | await removeUserSubscription(servers[0].url, users[0].accessToken, rootChannelNameServer1 + '@localhost:9001') | ||
253 | |||
254 | await waitJobs(servers) | ||
255 | |||
256 | { | ||
257 | const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt') | ||
258 | expect(res.body.total).to.equal(0) | ||
259 | |||
260 | const videos: Video[] = res.body.data | ||
261 | expect(videos).to.be.an('array') | ||
262 | expect(videos).to.have.lengthOf(0) | ||
263 | } | ||
264 | }) | ||
265 | |||
266 | it('Should correctly display public videos on server 1', async function () { | ||
267 | const res = await getVideosList(servers[0].url) | ||
268 | |||
269 | expect(res.body.total).to.equal(5) | ||
270 | for (const video of res.body.data) { | ||
271 | expect(video.name).to.not.contain('1-3') | ||
272 | expect(video.name).to.not.contain('2-3') | ||
273 | expect(video.name).to.not.contain('video server 3 added after follow') | ||
274 | } | ||
275 | }) | ||
276 | |||
277 | it('Should follow user of server 3 again', async function () { | ||
278 | this.timeout(30000) | ||
279 | |||
280 | await addUserSubscription(servers[0].url, users[0].accessToken, users[2].videoChannelName + '@localhost:9003') | ||
281 | |||
282 | await waitJobs(servers) | ||
283 | |||
284 | { | ||
285 | const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt') | ||
286 | expect(res.body.total).to.equal(3) | ||
287 | |||
288 | const videos: Video[] = res.body.data | ||
289 | expect(videos).to.be.an('array') | ||
290 | expect(videos).to.have.lengthOf(3) | ||
291 | |||
292 | expect(videos[0].name).to.equal('video 1-3') | ||
293 | expect(videos[1].name).to.equal('video 2-3') | ||
294 | expect(videos[2].name).to.equal('video server 3 added after follow') | ||
295 | } | ||
296 | |||
297 | { | ||
298 | const res = await getVideosList(servers[0].url) | ||
299 | |||
300 | expect(res.body.total).to.equal(5) | ||
301 | for (const video of res.body.data) { | ||
302 | expect(video.name).to.not.contain('1-3') | ||
303 | expect(video.name).to.not.contain('2-3') | ||
304 | expect(video.name).to.not.contain('video server 3 added after follow') | ||
305 | } | ||
306 | } | ||
307 | }) | ||
308 | |||
309 | after(async function () { | ||
310 | killallServers(servers) | ||
311 | }) | ||
312 | }) | ||
diff --git a/server/tests/utils/users/user-subscriptions.ts b/server/tests/utils/users/user-subscriptions.ts new file mode 100644 index 000000000..323e5de58 --- /dev/null +++ b/server/tests/utils/users/user-subscriptions.ts | |||
@@ -0,0 +1,57 @@ | |||
1 | import { makeDeleteRequest, makeGetRequest, makePostBodyRequest } from '../' | ||
2 | |||
3 | function addUserSubscription (url: string, token: string, targetUri: string, statusCodeExpected = 204) { | ||
4 | const path = '/api/v1/users/me/subscriptions' | ||
5 | |||
6 | return makePostBodyRequest({ | ||
7 | url, | ||
8 | path, | ||
9 | token, | ||
10 | statusCodeExpected, | ||
11 | fields: { uri: targetUri } | ||
12 | }) | ||
13 | } | ||
14 | |||
15 | function listUserSubscriptions (url: string, token: string, sort = '-createdAt', statusCodeExpected = 200) { | ||
16 | const path = '/api/v1/users/me/subscriptions' | ||
17 | |||
18 | return makeGetRequest({ | ||
19 | url, | ||
20 | path, | ||
21 | token, | ||
22 | statusCodeExpected, | ||
23 | query: { sort } | ||
24 | }) | ||
25 | } | ||
26 | |||
27 | function listUserSubscriptionVideos (url: string, token: string, sort = '-createdAt', statusCodeExpected = 200) { | ||
28 | const path = '/api/v1/users/me/subscriptions/videos' | ||
29 | |||
30 | return makeGetRequest({ | ||
31 | url, | ||
32 | path, | ||
33 | token, | ||
34 | statusCodeExpected, | ||
35 | query: { sort } | ||
36 | }) | ||
37 | } | ||
38 | |||
39 | function removeUserSubscription (url: string, token: string, uri: string, statusCodeExpected = 204) { | ||
40 | const path = '/api/v1/users/me/subscriptions/' + uri | ||
41 | |||
42 | return makeDeleteRequest({ | ||
43 | url, | ||
44 | path, | ||
45 | token, | ||
46 | statusCodeExpected | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | // --------------------------------------------------------------------------- | ||
51 | |||
52 | export { | ||
53 | addUserSubscription, | ||
54 | listUserSubscriptions, | ||
55 | listUserSubscriptionVideos, | ||
56 | removeUserSubscription | ||
57 | } | ||