diff options
Diffstat (limited to 'server')
336 files changed, 13823 insertions, 5205 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 433186179..31c0a5fbd 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -3,7 +3,7 @@ import * as express from 'express' | |||
3 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' | 3 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' |
4 | import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' | 4 | import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' |
5 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers' | 5 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers' |
6 | import { buildAnnounceWithVideoAudience } from '../../lib/activitypub/send' | 6 | import { buildAnnounceWithVideoAudience, buildLikeActivity } from '../../lib/activitypub/send' |
7 | import { audiencify, getAudience } from '../../lib/activitypub/audience' | 7 | import { audiencify, getAudience } from '../../lib/activitypub/audience' |
8 | import { buildCreateActivity } from '../../lib/activitypub/send/send-create' | 8 | import { buildCreateActivity } from '../../lib/activitypub/send/send-create' |
9 | import { | 9 | import { |
@@ -11,9 +11,10 @@ import { | |||
11 | executeIfActivityPub, | 11 | executeIfActivityPub, |
12 | localAccountValidator, | 12 | localAccountValidator, |
13 | localVideoChannelValidator, | 13 | localVideoChannelValidator, |
14 | videosCustomGetValidator | 14 | videosCustomGetValidator, |
15 | videosShareValidator | ||
15 | } from '../../middlewares' | 16 | } from '../../middlewares' |
16 | import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators' | 17 | import { getAccountVideoRateValidator, videoCommentGetValidator, videosGetValidator } from '../../middlewares/validators' |
17 | import { AccountModel } from '../../models/account/account' | 18 | import { AccountModel } from '../../models/account/account' |
18 | import { ActorModel } from '../../models/activitypub/actor' | 19 | import { ActorModel } from '../../models/activitypub/actor' |
19 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 20 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' |
@@ -25,14 +26,17 @@ import { cacheRoute } from '../../middlewares/cache' | |||
25 | import { activityPubResponse } from './utils' | 26 | import { activityPubResponse } from './utils' |
26 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 27 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
27 | import { | 28 | import { |
29 | getRateUrl, | ||
28 | getVideoCommentsActivityPubUrl, | 30 | getVideoCommentsActivityPubUrl, |
29 | getVideoDislikesActivityPubUrl, | 31 | getVideoDislikesActivityPubUrl, |
30 | getVideoLikesActivityPubUrl, | 32 | getVideoLikesActivityPubUrl, |
31 | getVideoSharesActivityPubUrl | 33 | getVideoSharesActivityPubUrl |
32 | } from '../../lib/activitypub' | 34 | } from '../../lib/activitypub' |
33 | import { VideoCaptionModel } from '../../models/video/video-caption' | 35 | import { VideoCaptionModel } from '../../models/video/video-caption' |
34 | import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' | 36 | import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy' |
35 | import { getServerActor } from '../../helpers/utils' | 37 | import { getServerActor } from '../../helpers/utils' |
38 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | ||
39 | import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike' | ||
36 | 40 | ||
37 | const activityPubClientRouter = express.Router() | 41 | const activityPubClientRouter = express.Router() |
38 | 42 | ||
@@ -48,21 +52,29 @@ activityPubClientRouter.get('/accounts?/:name/following', | |||
48 | executeIfActivityPub(asyncMiddleware(localAccountValidator)), | 52 | executeIfActivityPub(asyncMiddleware(localAccountValidator)), |
49 | executeIfActivityPub(asyncMiddleware(accountFollowingController)) | 53 | executeIfActivityPub(asyncMiddleware(accountFollowingController)) |
50 | ) | 54 | ) |
55 | activityPubClientRouter.get('/accounts?/:name/likes/:videoId', | ||
56 | executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('like'))), | ||
57 | executeIfActivityPub(getAccountVideoRate('like')) | ||
58 | ) | ||
59 | activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', | ||
60 | executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('dislike'))), | ||
61 | executeIfActivityPub(getAccountVideoRate('dislike')) | ||
62 | ) | ||
51 | 63 | ||
52 | activityPubClientRouter.get('/videos/watch/:id', | 64 | activityPubClientRouter.get('/videos/watch/:id', |
53 | executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), | 65 | executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), |
54 | executeIfActivityPub(asyncMiddleware(videosGetValidator)), | 66 | executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video-with-rights'))), |
55 | executeIfActivityPub(asyncMiddleware(videoController)) | 67 | executeIfActivityPub(asyncMiddleware(videoController)) |
56 | ) | 68 | ) |
57 | activityPubClientRouter.get('/videos/watch/:id/activity', | 69 | activityPubClientRouter.get('/videos/watch/:id/activity', |
58 | executeIfActivityPub(asyncMiddleware(videosGetValidator)), | 70 | executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video-with-rights'))), |
59 | executeIfActivityPub(asyncMiddleware(videoController)) | 71 | executeIfActivityPub(asyncMiddleware(videoController)) |
60 | ) | 72 | ) |
61 | activityPubClientRouter.get('/videos/watch/:id/announces', | 73 | activityPubClientRouter.get('/videos/watch/:id/announces', |
62 | executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), | 74 | executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), |
63 | executeIfActivityPub(asyncMiddleware(videoAnnouncesController)) | 75 | executeIfActivityPub(asyncMiddleware(videoAnnouncesController)) |
64 | ) | 76 | ) |
65 | activityPubClientRouter.get('/videos/watch/:id/announces/:accountId', | 77 | activityPubClientRouter.get('/videos/watch/:id/announces/:actorId', |
66 | executeIfActivityPub(asyncMiddleware(videosShareValidator)), | 78 | executeIfActivityPub(asyncMiddleware(videosShareValidator)), |
67 | executeIfActivityPub(asyncMiddleware(videoAnnounceController)) | 79 | executeIfActivityPub(asyncMiddleware(videoAnnounceController)) |
68 | ) | 80 | ) |
@@ -101,7 +113,11 @@ activityPubClientRouter.get('/video-channels/:name/following', | |||
101 | ) | 113 | ) |
102 | 114 | ||
103 | activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', | 115 | activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', |
104 | executeIfActivityPub(asyncMiddleware(videoRedundancyGetValidator)), | 116 | executeIfActivityPub(asyncMiddleware(videoFileRedundancyGetValidator)), |
117 | executeIfActivityPub(asyncMiddleware(videoRedundancyController)) | ||
118 | ) | ||
119 | activityPubClientRouter.get('/redundancy/video-playlists/:streamingPlaylistType/:videoId', | ||
120 | executeIfActivityPub(asyncMiddleware(videoPlaylistRedundancyGetValidator)), | ||
105 | executeIfActivityPub(asyncMiddleware(videoRedundancyController)) | 121 | executeIfActivityPub(asyncMiddleware(videoRedundancyController)) |
106 | ) | 122 | ) |
107 | 123 | ||
@@ -133,8 +149,25 @@ async function accountFollowingController (req: express.Request, res: express.Re | |||
133 | return activityPubResponse(activityPubContextify(activityPubResult), res) | 149 | return activityPubResponse(activityPubContextify(activityPubResult), res) |
134 | } | 150 | } |
135 | 151 | ||
136 | async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { | 152 | function getAccountVideoRate (rateType: VideoRateType) { |
137 | const video: VideoModel = res.locals.video | 153 | return (req: express.Request, res: express.Response) => { |
154 | const accountVideoRate: AccountVideoRateModel = res.locals.accountVideoRate | ||
155 | |||
156 | const byActor = accountVideoRate.Account.Actor | ||
157 | const url = getRateUrl(rateType, byActor, accountVideoRate.Video) | ||
158 | const APObject = rateType === 'like' | ||
159 | ? buildLikeActivity(url, byActor, accountVideoRate.Video) | ||
160 | : buildDislikeActivity(url, byActor, accountVideoRate.Video) | ||
161 | |||
162 | return activityPubResponse(activityPubContextify(APObject), res) | ||
163 | } | ||
164 | } | ||
165 | |||
166 | async function videoController (req: express.Request, res: express.Response) { | ||
167 | // We need more attributes | ||
168 | const video: VideoModel = await VideoModel.loadForGetAPI(res.locals.video.id) | ||
169 | |||
170 | if (video.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(video.url) | ||
138 | 171 | ||
139 | // We need captions to render AP object | 172 | // We need captions to render AP object |
140 | video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id) | 173 | video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id) |
@@ -150,14 +183,17 @@ async function videoController (req: express.Request, res: express.Response, nex | |||
150 | return activityPubResponse(activityPubContextify(videoObject), res) | 183 | return activityPubResponse(activityPubContextify(videoObject), res) |
151 | } | 184 | } |
152 | 185 | ||
153 | async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { | 186 | async function videoAnnounceController (req: express.Request, res: express.Response) { |
154 | const share = res.locals.videoShare as VideoShareModel | 187 | const share = res.locals.videoShare as VideoShareModel |
188 | |||
189 | if (share.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(share.url) | ||
190 | |||
155 | const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined) | 191 | const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined) |
156 | 192 | ||
157 | return activityPubResponse(activityPubContextify(activity), res) | 193 | return activityPubResponse(activityPubContextify(activity), res) |
158 | } | 194 | } |
159 | 195 | ||
160 | async function videoAnnouncesController (req: express.Request, res: express.Response, next: express.NextFunction) { | 196 | async function videoAnnouncesController (req: express.Request, res: express.Response) { |
161 | const video: VideoModel = res.locals.video | 197 | const video: VideoModel = res.locals.video |
162 | 198 | ||
163 | const handler = async (start: number, count: number) => { | 199 | const handler = async (start: number, count: number) => { |
@@ -172,21 +208,21 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp | |||
172 | return activityPubResponse(activityPubContextify(json), res) | 208 | return activityPubResponse(activityPubContextify(json), res) |
173 | } | 209 | } |
174 | 210 | ||
175 | async function videoLikesController (req: express.Request, res: express.Response, next: express.NextFunction) { | 211 | async function videoLikesController (req: express.Request, res: express.Response) { |
176 | const video: VideoModel = res.locals.video | 212 | const video: VideoModel = res.locals.video |
177 | const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video)) | 213 | const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video)) |
178 | 214 | ||
179 | return activityPubResponse(activityPubContextify(json), res) | 215 | return activityPubResponse(activityPubContextify(json), res) |
180 | } | 216 | } |
181 | 217 | ||
182 | async function videoDislikesController (req: express.Request, res: express.Response, next: express.NextFunction) { | 218 | async function videoDislikesController (req: express.Request, res: express.Response) { |
183 | const video: VideoModel = res.locals.video | 219 | const video: VideoModel = res.locals.video |
184 | const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video)) | 220 | const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video)) |
185 | 221 | ||
186 | return activityPubResponse(activityPubContextify(json), res) | 222 | return activityPubResponse(activityPubContextify(json), res) |
187 | } | 223 | } |
188 | 224 | ||
189 | async function videoCommentsController (req: express.Request, res: express.Response, next: express.NextFunction) { | 225 | async function videoCommentsController (req: express.Request, res: express.Response) { |
190 | const video: VideoModel = res.locals.video | 226 | const video: VideoModel = res.locals.video |
191 | 227 | ||
192 | const handler = async (start: number, count: number) => { | 228 | const handler = async (start: number, count: number) => { |
@@ -201,29 +237,31 @@ async function videoCommentsController (req: express.Request, res: express.Respo | |||
201 | return activityPubResponse(activityPubContextify(json), res) | 237 | return activityPubResponse(activityPubContextify(json), res) |
202 | } | 238 | } |
203 | 239 | ||
204 | async function videoChannelController (req: express.Request, res: express.Response, next: express.NextFunction) { | 240 | async function videoChannelController (req: express.Request, res: express.Response) { |
205 | const videoChannel: VideoChannelModel = res.locals.videoChannel | 241 | const videoChannel: VideoChannelModel = res.locals.videoChannel |
206 | 242 | ||
207 | return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res) | 243 | return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res) |
208 | } | 244 | } |
209 | 245 | ||
210 | async function videoChannelFollowersController (req: express.Request, res: express.Response, next: express.NextFunction) { | 246 | async function videoChannelFollowersController (req: express.Request, res: express.Response) { |
211 | const videoChannel: VideoChannelModel = res.locals.videoChannel | 247 | const videoChannel: VideoChannelModel = res.locals.videoChannel |
212 | const activityPubResult = await actorFollowers(req, videoChannel.Actor) | 248 | const activityPubResult = await actorFollowers(req, videoChannel.Actor) |
213 | 249 | ||
214 | return activityPubResponse(activityPubContextify(activityPubResult), res) | 250 | return activityPubResponse(activityPubContextify(activityPubResult), res) |
215 | } | 251 | } |
216 | 252 | ||
217 | async function videoChannelFollowingController (req: express.Request, res: express.Response, next: express.NextFunction) { | 253 | async function videoChannelFollowingController (req: express.Request, res: express.Response) { |
218 | const videoChannel: VideoChannelModel = res.locals.videoChannel | 254 | const videoChannel: VideoChannelModel = res.locals.videoChannel |
219 | const activityPubResult = await actorFollowing(req, videoChannel.Actor) | 255 | const activityPubResult = await actorFollowing(req, videoChannel.Actor) |
220 | 256 | ||
221 | return activityPubResponse(activityPubContextify(activityPubResult), res) | 257 | return activityPubResponse(activityPubContextify(activityPubResult), res) |
222 | } | 258 | } |
223 | 259 | ||
224 | async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) { | 260 | async function videoCommentController (req: express.Request, res: express.Response) { |
225 | const videoComment: VideoCommentModel = res.locals.videoComment | 261 | const videoComment: VideoCommentModel = res.locals.videoComment |
226 | 262 | ||
263 | if (videoComment.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(videoComment.url) | ||
264 | |||
227 | const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) | 265 | const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) |
228 | const isPublic = true // Comments are always public | 266 | const isPublic = true // Comments are always public |
229 | const audience = getAudience(videoComment.Account.Actor, isPublic) | 267 | const audience = getAudience(videoComment.Account.Actor, isPublic) |
@@ -239,7 +277,9 @@ async function videoCommentController (req: express.Request, res: express.Respon | |||
239 | } | 277 | } |
240 | 278 | ||
241 | async function videoRedundancyController (req: express.Request, res: express.Response) { | 279 | async function videoRedundancyController (req: express.Request, res: express.Response) { |
242 | const videoRedundancy = res.locals.videoRedundancy | 280 | const videoRedundancy: VideoRedundancyModel = res.locals.videoRedundancy |
281 | if (videoRedundancy.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(videoRedundancy.url) | ||
282 | |||
243 | const serverActor = await getServerActor() | 283 | const serverActor = await getServerActor() |
244 | 284 | ||
245 | const audience = getAudience(serverActor) | 285 | const audience = getAudience(serverActor) |
@@ -260,7 +300,7 @@ async function actorFollowing (req: express.Request, actor: ActorModel) { | |||
260 | return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count) | 300 | return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count) |
261 | } | 301 | } |
262 | 302 | ||
263 | return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page) | 303 | return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page) |
264 | } | 304 | } |
265 | 305 | ||
266 | async function actorFollowers (req: express.Request, actor: ActorModel) { | 306 | async function actorFollowers (req: express.Request, actor: ActorModel) { |
@@ -268,7 +308,7 @@ async function actorFollowers (req: express.Request, actor: ActorModel) { | |||
268 | return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count) | 308 | return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count) |
269 | } | 309 | } |
270 | 310 | ||
271 | return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page) | 311 | return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page) |
272 | } | 312 | } |
273 | 313 | ||
274 | function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) { | 314 | function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) { |
@@ -276,7 +316,7 @@ function videoRates (req: express.Request, rateType: VideoRateType, video: Video | |||
276 | const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) | 316 | const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) |
277 | return { | 317 | return { |
278 | total: result.count, | 318 | total: result.count, |
279 | data: result.rows.map(r => r.Account.Actor.url) | 319 | data: result.rows.map(r => r.url) |
280 | } | 320 | } |
281 | } | 321 | } |
282 | return activityPubCollectionPagination(url, handler, req.query.page) | 322 | return activityPubCollectionPagination(url, handler, req.query.page) |
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts index 738d155eb..f0e65015b 100644 --- a/server/controllers/activitypub/inbox.ts +++ b/server/controllers/activitypub/inbox.ts | |||
@@ -43,11 +43,13 @@ export { | |||
43 | // --------------------------------------------------------------------------- | 43 | // --------------------------------------------------------------------------- |
44 | 44 | ||
45 | const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => { | 45 | const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => { |
46 | processActivities(task.activities, task.signatureActor, task.inboxActor) | 46 | const options = { signatureActor: task.signatureActor, inboxActor: task.inboxActor } |
47 | |||
48 | processActivities(task.activities, options) | ||
47 | .then(() => cb()) | 49 | .then(() => cb()) |
48 | }) | 50 | }) |
49 | 51 | ||
50 | function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) { | 52 | function inboxController (req: express.Request, res: express.Response) { |
51 | const rootActivity: RootActivity = req.body | 53 | const rootActivity: RootActivity = req.body |
52 | let activities: Activity[] = [] | 54 | let activities: Activity[] = [] |
53 | 55 | ||
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index b7691ccba..8c0237203 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { getFormattedObjects } from '../../helpers/utils' | 2 | import { getFormattedObjects } from '../../helpers/utils' |
3 | import { | 3 | import { |
4 | asyncMiddleware, commonVideosFiltersValidator, | 4 | asyncMiddleware, |
5 | commonVideosFiltersValidator, | ||
5 | listVideoAccountChannelsValidator, | 6 | listVideoAccountChannelsValidator, |
6 | optionalAuthenticate, | 7 | optionalAuthenticate, |
7 | paginationValidator, | 8 | paginationValidator, |
@@ -13,6 +14,8 @@ import { AccountModel } from '../../models/account/account' | |||
13 | import { VideoModel } from '../../models/video/video' | 14 | import { VideoModel } from '../../models/video/video' |
14 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 15 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
15 | import { VideoChannelModel } from '../../models/video/video-channel' | 16 | import { VideoChannelModel } from '../../models/video/video-channel' |
17 | import { JobQueue } from '../../lib/job-queue' | ||
18 | import { logger } from '../../helpers/logger' | ||
16 | 19 | ||
17 | const accountsRouter = express.Router() | 20 | const accountsRouter = express.Router() |
18 | 21 | ||
@@ -56,6 +59,11 @@ export { | |||
56 | function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { | 59 | function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { |
57 | const account: AccountModel = res.locals.account | 60 | const account: AccountModel = res.locals.account |
58 | 61 | ||
62 | if (account.isOutdated()) { | ||
63 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: account.Actor.url } }) | ||
64 | .catch(err => logger.error('Cannot create AP refresher job for actor %s.', account.Actor.url, { err })) | ||
65 | } | ||
66 | |||
59 | return res.json(account.toFormattedJSON()) | 67 | return res.json(account.toFormattedJSON()) |
60 | } | 68 | } |
61 | 69 | ||
@@ -73,10 +81,10 @@ async function listVideoAccountChannels (req: express.Request, res: express.Resp | |||
73 | 81 | ||
74 | async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 82 | async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
75 | const account: AccountModel = res.locals.account | 83 | const account: AccountModel = res.locals.account |
76 | const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | 84 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined |
77 | 85 | ||
78 | const resultList = await VideoModel.listForApi({ | 86 | const resultList = await VideoModel.listForApi({ |
79 | actorId, | 87 | followerActorId, |
80 | start: req.query.start, | 88 | start: req.query.start, |
81 | count: req.query.count, | 89 | count: req.query.count, |
82 | sort: req.query.sort, | 90 | sort: req.query.sort, |
@@ -86,9 +94,11 @@ async function listAccountVideos (req: express.Request, res: express.Response, n | |||
86 | languageOneOf: req.query.languageOneOf, | 94 | languageOneOf: req.query.languageOneOf, |
87 | tagsOneOf: req.query.tagsOneOf, | 95 | tagsOneOf: req.query.tagsOneOf, |
88 | tagsAllOf: req.query.tagsAllOf, | 96 | tagsAllOf: req.query.tagsAllOf, |
97 | filter: req.query.filter, | ||
89 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 98 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
90 | withFiles: false, | 99 | withFiles: false, |
91 | accountId: account.id | 100 | accountId: account.id, |
101 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
92 | }) | 102 | }) |
93 | 103 | ||
94 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 104 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 03c1cec7b..1f3341bc0 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { omit } from 'lodash' | 2 | import { snakeCase } from 'lodash' |
3 | import { ServerConfig, UserRight } from '../../../shared' | 3 | import { ServerConfig, UserRight } from '../../../shared' |
4 | import { About } from '../../../shared/models/server/about.model' | 4 | import { About } from '../../../shared/models/server/about.model' |
5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' | 5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' |
@@ -10,7 +10,10 @@ import { customConfigUpdateValidator } from '../../middlewares/validators/config | |||
10 | import { ClientHtml } from '../../lib/client-html' | 10 | import { ClientHtml } from '../../lib/client-html' |
11 | import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' | 11 | import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' |
12 | import { remove, writeJSON } from 'fs-extra' | 12 | import { remove, writeJSON } from 'fs-extra' |
13 | import { getVersion } from '../../helpers/utils' | 13 | import { getServerCommit } from '../../helpers/utils' |
14 | import { Emailer } from '../../lib/emailer' | ||
15 | import { isNumeric } from 'validator' | ||
16 | import { objectConverter } from '../../helpers/core-utils' | ||
14 | 17 | ||
15 | const packageJSON = require('../../../../package.json') | 18 | const packageJSON = require('../../../../package.json') |
16 | const configRouter = express.Router() | 19 | const configRouter = express.Router() |
@@ -40,11 +43,11 @@ configRouter.delete('/custom', | |||
40 | ) | 43 | ) |
41 | 44 | ||
42 | let serverCommit: string | 45 | let serverCommit: string |
43 | async function getConfig (req: express.Request, res: express.Response, next: express.NextFunction) { | 46 | async function getConfig (req: express.Request, res: express.Response) { |
44 | const allowed = await isSignupAllowed() | 47 | const allowed = await isSignupAllowed() |
45 | const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip) | 48 | const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip) |
46 | serverCommit = (serverCommit) ? serverCommit : await getVersion() | 49 | |
47 | if (serverCommit === packageJSON.version) serverCommit = '' | 50 | if (serverCommit === undefined) serverCommit = await getServerCommit() |
48 | 51 | ||
49 | const enabledResolutions = Object.keys(CONFIG.TRANSCODING.RESOLUTIONS) | 52 | const enabledResolutions = Object.keys(CONFIG.TRANSCODING.RESOLUTIONS) |
50 | .filter(key => CONFIG.TRANSCODING.ENABLED === CONFIG.TRANSCODING.RESOLUTIONS[key] === true) | 53 | .filter(key => CONFIG.TRANSCODING.ENABLED === CONFIG.TRANSCODING.RESOLUTIONS[key] === true) |
@@ -61,6 +64,12 @@ async function getConfig (req: express.Request, res: express.Response, next: exp | |||
61 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS | 64 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS |
62 | } | 65 | } |
63 | }, | 66 | }, |
67 | email: { | ||
68 | enabled: Emailer.isEnabled() | ||
69 | }, | ||
70 | contactForm: { | ||
71 | enabled: CONFIG.CONTACT_FORM.ENABLED | ||
72 | }, | ||
64 | serverVersion: packageJSON.version, | 73 | serverVersion: packageJSON.version, |
65 | serverCommit, | 74 | serverCommit, |
66 | signup: { | 75 | signup: { |
@@ -69,6 +78,9 @@ async function getConfig (req: express.Request, res: express.Response, next: exp | |||
69 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | 78 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION |
70 | }, | 79 | }, |
71 | transcoding: { | 80 | transcoding: { |
81 | hls: { | ||
82 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | ||
83 | }, | ||
72 | enabledResolutions | 84 | enabledResolutions |
73 | }, | 85 | }, |
74 | import: { | 86 | import: { |
@@ -111,6 +123,11 @@ async function getConfig (req: express.Request, res: express.Response, next: exp | |||
111 | user: { | 123 | user: { |
112 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | 124 | videoQuota: CONFIG.USER.VIDEO_QUOTA, |
113 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | 125 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY |
126 | }, | ||
127 | trending: { | ||
128 | videos: { | ||
129 | intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS | ||
130 | } | ||
114 | } | 131 | } |
115 | } | 132 | } |
116 | 133 | ||
@@ -150,32 +167,10 @@ async function deleteCustomConfig (req: express.Request, res: express.Response, | |||
150 | } | 167 | } |
151 | 168 | ||
152 | async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { | 169 | async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { |
153 | const toUpdate: CustomConfig = req.body | ||
154 | const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig()) | 170 | const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig()) |
155 | 171 | ||
156 | // Force number conversion | 172 | // camelCase to snake_case key + Force number conversion |
157 | toUpdate.cache.previews.size = parseInt('' + toUpdate.cache.previews.size, 10) | 173 | const toUpdateJSON = convertCustomConfigBody(req.body) |
158 | toUpdate.cache.captions.size = parseInt('' + toUpdate.cache.captions.size, 10) | ||
159 | toUpdate.signup.limit = parseInt('' + toUpdate.signup.limit, 10) | ||
160 | toUpdate.user.videoQuota = parseInt('' + toUpdate.user.videoQuota, 10) | ||
161 | toUpdate.user.videoQuotaDaily = parseInt('' + toUpdate.user.videoQuotaDaily, 10) | ||
162 | toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10) | ||
163 | |||
164 | // camelCase to snake_case key | ||
165 | const toUpdateJSON = omit( | ||
166 | toUpdate, | ||
167 | 'user.videoQuota', | ||
168 | 'instance.defaultClientRoute', | ||
169 | 'instance.shortDescription', | ||
170 | 'cache.videoCaptions', | ||
171 | 'signup.requiresEmailVerification' | ||
172 | ) | ||
173 | toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota | ||
174 | toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily | ||
175 | toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute | ||
176 | toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription | ||
177 | toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy | ||
178 | toUpdateJSON.signup['requires_email_verification'] = toUpdate.signup.requiresEmailVerification | ||
179 | 174 | ||
180 | await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 }) | 175 | await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 }) |
181 | 176 | ||
@@ -237,12 +232,16 @@ function customConfig (): CustomConfig { | |||
237 | admin: { | 232 | admin: { |
238 | email: CONFIG.ADMIN.EMAIL | 233 | email: CONFIG.ADMIN.EMAIL |
239 | }, | 234 | }, |
235 | contactForm: { | ||
236 | enabled: CONFIG.CONTACT_FORM.ENABLED | ||
237 | }, | ||
240 | user: { | 238 | user: { |
241 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | 239 | videoQuota: CONFIG.USER.VIDEO_QUOTA, |
242 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | 240 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY |
243 | }, | 241 | }, |
244 | transcoding: { | 242 | transcoding: { |
245 | enabled: CONFIG.TRANSCODING.ENABLED, | 243 | enabled: CONFIG.TRANSCODING.ENABLED, |
244 | allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, | ||
246 | threads: CONFIG.TRANSCODING.THREADS, | 245 | threads: CONFIG.TRANSCODING.THREADS, |
247 | resolutions: { | 246 | resolutions: { |
248 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], | 247 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], |
@@ -250,6 +249,9 @@ function customConfig (): CustomConfig { | |||
250 | '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], | 249 | '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], |
251 | '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ], | 250 | '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ], |
252 | '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ] | 251 | '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ] |
252 | }, | ||
253 | hls: { | ||
254 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | ||
253 | } | 255 | } |
254 | }, | 256 | }, |
255 | import: { | 257 | import: { |
@@ -264,3 +266,20 @@ function customConfig (): CustomConfig { | |||
264 | } | 266 | } |
265 | } | 267 | } |
266 | } | 268 | } |
269 | |||
270 | function convertCustomConfigBody (body: CustomConfig) { | ||
271 | function keyConverter (k: string) { | ||
272 | // Transcoding resolutions exception | ||
273 | if (/^\d{3,4}p$/.exec(k)) return k | ||
274 | |||
275 | return snakeCase(k) | ||
276 | } | ||
277 | |||
278 | function valueConverter (v: any) { | ||
279 | if (isNumeric(v + '')) return parseInt('' + v, 10) | ||
280 | |||
281 | return v | ||
282 | } | ||
283 | |||
284 | return objectConverter(body, keyConverter, valueConverter) | ||
285 | } | ||
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts index 4be2b5ef7..534305ba6 100644 --- a/server/controllers/api/search.ts +++ b/server/controllers/api/search.ts | |||
@@ -118,7 +118,8 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response) | |||
118 | const options = Object.assign(query, { | 118 | const options = Object.assign(query, { |
119 | includeLocalVideos: true, | 119 | includeLocalVideos: true, |
120 | nsfw: buildNSFWFilter(res, query.nsfw), | 120 | nsfw: buildNSFWFilter(res, query.nsfw), |
121 | userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 121 | filter: query.filter, |
122 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
122 | }) | 123 | }) |
123 | const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) | 124 | const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) |
124 | 125 | ||
diff --git a/server/controllers/api/server/contact.ts b/server/controllers/api/server/contact.ts new file mode 100644 index 000000000..b1144c94e --- /dev/null +++ b/server/controllers/api/server/contact.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import * as express from 'express' | ||
2 | import { asyncMiddleware, contactAdministratorValidator } from '../../../middlewares' | ||
3 | import { Redis } from '../../../lib/redis' | ||
4 | import { Emailer } from '../../../lib/emailer' | ||
5 | import { ContactForm } from '../../../../shared/models/server' | ||
6 | |||
7 | const contactRouter = express.Router() | ||
8 | |||
9 | contactRouter.post('/contact', | ||
10 | asyncMiddleware(contactAdministratorValidator), | ||
11 | asyncMiddleware(contactAdministrator) | ||
12 | ) | ||
13 | |||
14 | async function contactAdministrator (req: express.Request, res: express.Response) { | ||
15 | const data = req.body as ContactForm | ||
16 | |||
17 | await Emailer.Instance.addContactFormJob(data.fromEmail, data.fromName, data.body) | ||
18 | |||
19 | await Redis.Instance.setContactFormIp(req.ip) | ||
20 | |||
21 | return res.status(204).end() | ||
22 | } | ||
23 | |||
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | export { | ||
27 | contactRouter | ||
28 | } | ||
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index d62400e42..9fa6c34ba 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts | |||
@@ -61,14 +61,26 @@ export { | |||
61 | 61 | ||
62 | async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { | 62 | async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { |
63 | const serverActor = await getServerActor() | 63 | const serverActor = await getServerActor() |
64 | const resultList = await ActorFollowModel.listFollowingForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) | 64 | const resultList = await ActorFollowModel.listFollowingForApi( |
65 | serverActor.id, | ||
66 | req.query.start, | ||
67 | req.query.count, | ||
68 | req.query.sort, | ||
69 | req.query.search | ||
70 | ) | ||
65 | 71 | ||
66 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 72 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
67 | } | 73 | } |
68 | 74 | ||
69 | async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { | 75 | async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { |
70 | const serverActor = await getServerActor() | 76 | const serverActor = await getServerActor() |
71 | const resultList = await ActorFollowModel.listFollowersForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) | 77 | const resultList = await ActorFollowModel.listFollowersForApi( |
78 | serverActor.id, | ||
79 | req.query.start, | ||
80 | req.query.count, | ||
81 | req.query.sort, | ||
82 | req.query.search | ||
83 | ) | ||
72 | 84 | ||
73 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 85 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
74 | } | 86 | } |
diff --git a/server/controllers/api/server/index.ts b/server/controllers/api/server/index.ts index 43bca2c10..814248e5f 100644 --- a/server/controllers/api/server/index.ts +++ b/server/controllers/api/server/index.ts | |||
@@ -2,12 +2,16 @@ import * as express from 'express' | |||
2 | import { serverFollowsRouter } from './follows' | 2 | import { serverFollowsRouter } from './follows' |
3 | import { statsRouter } from './stats' | 3 | import { statsRouter } from './stats' |
4 | import { serverRedundancyRouter } from './redundancy' | 4 | import { serverRedundancyRouter } from './redundancy' |
5 | import { serverBlocklistRouter } from './server-blocklist' | ||
6 | import { contactRouter } from './contact' | ||
5 | 7 | ||
6 | const serverRouter = express.Router() | 8 | const serverRouter = express.Router() |
7 | 9 | ||
8 | serverRouter.use('/', serverFollowsRouter) | 10 | serverRouter.use('/', serverFollowsRouter) |
9 | serverRouter.use('/', serverRedundancyRouter) | 11 | serverRouter.use('/', serverRedundancyRouter) |
10 | serverRouter.use('/', statsRouter) | 12 | serverRouter.use('/', statsRouter) |
13 | serverRouter.use('/', serverBlocklistRouter) | ||
14 | serverRouter.use('/', contactRouter) | ||
11 | 15 | ||
12 | // --------------------------------------------------------------------------- | 16 | // --------------------------------------------------------------------------- |
13 | 17 | ||
diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts new file mode 100644 index 000000000..3cb3a96e2 --- /dev/null +++ b/server/controllers/api/server/server-blocklist.ts | |||
@@ -0,0 +1,132 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | ||
3 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' | ||
4 | import { | ||
5 | asyncMiddleware, | ||
6 | asyncRetryTransactionMiddleware, | ||
7 | authenticate, | ||
8 | ensureUserHasRight, | ||
9 | paginationValidator, | ||
10 | setDefaultPagination, | ||
11 | setDefaultSort | ||
12 | } from '../../../middlewares' | ||
13 | import { | ||
14 | accountsBlocklistSortValidator, | ||
15 | blockAccountValidator, | ||
16 | blockServerValidator, | ||
17 | serversBlocklistSortValidator, | ||
18 | unblockAccountByServerValidator, | ||
19 | unblockServerByServerValidator | ||
20 | } from '../../../middlewares/validators' | ||
21 | import { AccountModel } from '../../../models/account/account' | ||
22 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' | ||
23 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' | ||
24 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' | ||
25 | import { ServerModel } from '../../../models/server/server' | ||
26 | import { UserRight } from '../../../../shared/models/users' | ||
27 | |||
28 | const serverBlocklistRouter = express.Router() | ||
29 | |||
30 | serverBlocklistRouter.get('/blocklist/accounts', | ||
31 | authenticate, | ||
32 | ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST), | ||
33 | paginationValidator, | ||
34 | accountsBlocklistSortValidator, | ||
35 | setDefaultSort, | ||
36 | setDefaultPagination, | ||
37 | asyncMiddleware(listBlockedAccounts) | ||
38 | ) | ||
39 | |||
40 | serverBlocklistRouter.post('/blocklist/accounts', | ||
41 | authenticate, | ||
42 | ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST), | ||
43 | asyncMiddleware(blockAccountValidator), | ||
44 | asyncRetryTransactionMiddleware(blockAccount) | ||
45 | ) | ||
46 | |||
47 | serverBlocklistRouter.delete('/blocklist/accounts/:accountName', | ||
48 | authenticate, | ||
49 | ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST), | ||
50 | asyncMiddleware(unblockAccountByServerValidator), | ||
51 | asyncRetryTransactionMiddleware(unblockAccount) | ||
52 | ) | ||
53 | |||
54 | serverBlocklistRouter.get('/blocklist/servers', | ||
55 | authenticate, | ||
56 | ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST), | ||
57 | paginationValidator, | ||
58 | serversBlocklistSortValidator, | ||
59 | setDefaultSort, | ||
60 | setDefaultPagination, | ||
61 | asyncMiddleware(listBlockedServers) | ||
62 | ) | ||
63 | |||
64 | serverBlocklistRouter.post('/blocklist/servers', | ||
65 | authenticate, | ||
66 | ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST), | ||
67 | asyncMiddleware(blockServerValidator), | ||
68 | asyncRetryTransactionMiddleware(blockServer) | ||
69 | ) | ||
70 | |||
71 | serverBlocklistRouter.delete('/blocklist/servers/:host', | ||
72 | authenticate, | ||
73 | ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST), | ||
74 | asyncMiddleware(unblockServerByServerValidator), | ||
75 | asyncRetryTransactionMiddleware(unblockServer) | ||
76 | ) | ||
77 | |||
78 | export { | ||
79 | serverBlocklistRouter | ||
80 | } | ||
81 | |||
82 | // --------------------------------------------------------------------------- | ||
83 | |||
84 | async function listBlockedAccounts (req: express.Request, res: express.Response) { | ||
85 | const serverActor = await getServerActor() | ||
86 | |||
87 | const resultList = await AccountBlocklistModel.listForApi(serverActor.Account.id, req.query.start, req.query.count, req.query.sort) | ||
88 | |||
89 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
90 | } | ||
91 | |||
92 | async function blockAccount (req: express.Request, res: express.Response) { | ||
93 | const serverActor = await getServerActor() | ||
94 | const accountToBlock: AccountModel = res.locals.account | ||
95 | |||
96 | await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id) | ||
97 | |||
98 | return res.status(204).end() | ||
99 | } | ||
100 | |||
101 | async function unblockAccount (req: express.Request, res: express.Response) { | ||
102 | const accountBlock: AccountBlocklistModel = res.locals.accountBlock | ||
103 | |||
104 | await removeAccountFromBlocklist(accountBlock) | ||
105 | |||
106 | return res.status(204).end() | ||
107 | } | ||
108 | |||
109 | async function listBlockedServers (req: express.Request, res: express.Response) { | ||
110 | const serverActor = await getServerActor() | ||
111 | |||
112 | const resultList = await ServerBlocklistModel.listForApi(serverActor.Account.id, req.query.start, req.query.count, req.query.sort) | ||
113 | |||
114 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
115 | } | ||
116 | |||
117 | async function blockServer (req: express.Request, res: express.Response) { | ||
118 | const serverActor = await getServerActor() | ||
119 | const serverToBlock: ServerModel = res.locals.server | ||
120 | |||
121 | await addServerInBlocklist(serverActor.Account.id, serverToBlock.id) | ||
122 | |||
123 | return res.status(204).end() | ||
124 | } | ||
125 | |||
126 | async function unblockServer (req: express.Request, res: express.Response) { | ||
127 | const serverBlock: ServerBlocklistModel = res.locals.serverBlock | ||
128 | |||
129 | await removeServerFromBlocklist(serverBlock) | ||
130 | |||
131 | return res.status(204).end() | ||
132 | } | ||
diff --git a/server/controllers/api/server/stats.ts b/server/controllers/api/server/stats.ts index 85803f69e..89ffd1717 100644 --- a/server/controllers/api/server/stats.ts +++ b/server/controllers/api/server/stats.ts | |||
@@ -8,6 +8,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment' | |||
8 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' | 8 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' |
9 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../../initializers/constants' | 9 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../../initializers/constants' |
10 | import { cacheRoute } from '../../../middlewares/cache' | 10 | import { cacheRoute } from '../../../middlewares/cache' |
11 | import { VideoFileModel } from '../../../models/video/video-file' | ||
11 | 12 | ||
12 | const statsRouter = express.Router() | 13 | const statsRouter = express.Router() |
13 | 14 | ||
@@ -16,11 +17,12 @@ statsRouter.get('/stats', | |||
16 | asyncMiddleware(getStats) | 17 | asyncMiddleware(getStats) |
17 | ) | 18 | ) |
18 | 19 | ||
19 | async function getStats (req: express.Request, res: express.Response, next: express.NextFunction) { | 20 | async function getStats (req: express.Request, res: express.Response) { |
20 | const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats() | 21 | const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats() |
21 | const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats() | 22 | const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats() |
22 | const { totalUsers } = await UserModel.getStats() | 23 | const { totalUsers } = await UserModel.getStats() |
23 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() | 24 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() |
25 | const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() | ||
24 | 26 | ||
25 | const videosRedundancyStats = await Promise.all( | 27 | const videosRedundancyStats = await Promise.all( |
26 | CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.map(r => { | 28 | CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.map(r => { |
@@ -32,8 +34,9 @@ async function getStats (req: express.Request, res: express.Response, next: expr | |||
32 | const data: ServerStats = { | 34 | const data: ServerStats = { |
33 | totalLocalVideos, | 35 | totalLocalVideos, |
34 | totalLocalVideoViews, | 36 | totalLocalVideoViews, |
35 | totalVideos, | 37 | totalLocalVideoFilesSize, |
36 | totalLocalVideoComments, | 38 | totalLocalVideoComments, |
39 | totalVideos, | ||
37 | totalVideoComments, | 40 | totalVideoComments, |
38 | totalUsers, | 41 | totalUsers, |
39 | totalInstanceFollowers, | 42 | totalInstanceFollowers, |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 0b0081520..e3533a7f6 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -37,6 +37,11 @@ import { UserModel } from '../../../models/account/user' | |||
37 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' | 37 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' |
38 | import { meRouter } from './me' | 38 | import { meRouter } from './me' |
39 | import { deleteUserToken } from '../../../lib/oauth-model' | 39 | import { deleteUserToken } from '../../../lib/oauth-model' |
40 | import { myBlocklistRouter } from './my-blocklist' | ||
41 | import { myVideosHistoryRouter } from './my-history' | ||
42 | import { myNotificationsRouter } from './my-notifications' | ||
43 | import { Notifier } from '../../../lib/notifier' | ||
44 | import { mySubscriptionsRouter } from './my-subscriptions' | ||
40 | 45 | ||
41 | const auditLogger = auditLoggerFactory('users') | 46 | const auditLogger = auditLoggerFactory('users') |
42 | 47 | ||
@@ -53,6 +58,10 @@ const askSendEmailLimiter = new RateLimit({ | |||
53 | }) | 58 | }) |
54 | 59 | ||
55 | const usersRouter = express.Router() | 60 | const usersRouter = express.Router() |
61 | usersRouter.use('/', myNotificationsRouter) | ||
62 | usersRouter.use('/', mySubscriptionsRouter) | ||
63 | usersRouter.use('/', myBlocklistRouter) | ||
64 | usersRouter.use('/', myVideosHistoryRouter) | ||
56 | usersRouter.use('/', meRouter) | 65 | usersRouter.use('/', meRouter) |
57 | 66 | ||
58 | usersRouter.get('/autocomplete', | 67 | usersRouter.get('/autocomplete', |
@@ -207,6 +216,8 @@ async function registerUser (req: express.Request, res: express.Response) { | |||
207 | await sendVerifyUserEmail(user) | 216 | await sendVerifyUserEmail(user) |
208 | } | 217 | } |
209 | 218 | ||
219 | Notifier.Instance.notifyOnNewUserRegistration(user) | ||
220 | |||
210 | return res.type('json').status(204).end() | 221 | return res.type('json').status(204).end() |
211 | } | 222 | } |
212 | 223 | ||
@@ -218,7 +229,7 @@ async function unblockUser (req: express.Request, res: express.Response, next: e | |||
218 | return res.status(204).end() | 229 | return res.status(204).end() |
219 | } | 230 | } |
220 | 231 | ||
221 | async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 232 | async function blockUser (req: express.Request, res: express.Response) { |
222 | const user: UserModel = res.locals.user | 233 | const user: UserModel = res.locals.user |
223 | const reason = req.body.reason | 234 | const reason = req.body.reason |
224 | 235 | ||
@@ -227,23 +238,23 @@ async function blockUser (req: express.Request, res: express.Response, next: exp | |||
227 | return res.status(204).end() | 238 | return res.status(204).end() |
228 | } | 239 | } |
229 | 240 | ||
230 | function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 241 | function getUser (req: express.Request, res: express.Response) { |
231 | return res.json((res.locals.user as UserModel).toFormattedJSON()) | 242 | return res.json((res.locals.user as UserModel).toFormattedJSON()) |
232 | } | 243 | } |
233 | 244 | ||
234 | async function autocompleteUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 245 | async function autocompleteUsers (req: express.Request, res: express.Response) { |
235 | const resultList = await UserModel.autoComplete(req.query.search as string) | 246 | const resultList = await UserModel.autoComplete(req.query.search as string) |
236 | 247 | ||
237 | return res.json(resultList) | 248 | return res.json(resultList) |
238 | } | 249 | } |
239 | 250 | ||
240 | async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 251 | async function listUsers (req: express.Request, res: express.Response) { |
241 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort) | 252 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search) |
242 | 253 | ||
243 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 254 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
244 | } | 255 | } |
245 | 256 | ||
246 | async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 257 | async function removeUser (req: express.Request, res: express.Response) { |
247 | const user: UserModel = res.locals.user | 258 | const user: UserModel = res.locals.user |
248 | 259 | ||
249 | await user.destroy() | 260 | await user.destroy() |
@@ -253,13 +264,15 @@ async function removeUser (req: express.Request, res: express.Response, next: ex | |||
253 | return res.sendStatus(204) | 264 | return res.sendStatus(204) |
254 | } | 265 | } |
255 | 266 | ||
256 | async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 267 | async function updateUser (req: express.Request, res: express.Response) { |
257 | const body: UserUpdate = req.body | 268 | const body: UserUpdate = req.body |
258 | const userToUpdate = res.locals.user as UserModel | 269 | const userToUpdate = res.locals.user as UserModel |
259 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) | 270 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) |
260 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role | 271 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role |
261 | 272 | ||
273 | if (body.password !== undefined) userToUpdate.password = body.password | ||
262 | if (body.email !== undefined) userToUpdate.email = body.email | 274 | if (body.email !== undefined) userToUpdate.email = body.email |
275 | if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified | ||
263 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota | 276 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota |
264 | if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily | 277 | if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily |
265 | if (body.role !== undefined) userToUpdate.role = body.role | 278 | if (body.role !== undefined) userToUpdate.role = body.role |
@@ -267,11 +280,11 @@ async function updateUser (req: express.Request, res: express.Response, next: ex | |||
267 | const user = await userToUpdate.save() | 280 | const user = await userToUpdate.save() |
268 | 281 | ||
269 | // Destroy user token to refresh rights | 282 | // Destroy user token to refresh rights |
270 | if (roleChanged) await deleteUserToken(userToUpdate.id) | 283 | if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id) |
271 | 284 | ||
272 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) | 285 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) |
273 | 286 | ||
274 | // Don't need to send this update to followers, these attributes are not propagated | 287 | // Don't need to send this update to followers, these attributes are not federated |
275 | 288 | ||
276 | return res.sendStatus(204) | 289 | return res.sendStatus(204) |
277 | } | 290 | } |
@@ -281,7 +294,7 @@ async function askResetUserPassword (req: express.Request, res: express.Response | |||
281 | 294 | ||
282 | const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) | 295 | const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) |
283 | const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString | 296 | const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString |
284 | await Emailer.Instance.addForgetPasswordEmailJob(user.email, url) | 297 | await Emailer.Instance.addPasswordResetEmailJob(user.email, url) |
285 | 298 | ||
286 | return res.status(204).end() | 299 | return res.status(204).end() |
287 | } | 300 | } |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 591ec6b25..d5e154869 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -2,47 +2,34 @@ import * as express from 'express' | |||
2 | import 'multer' | 2 | import 'multer' |
3 | import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' | 3 | import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' |
4 | import { getFormattedObjects } from '../../../helpers/utils' | 4 | import { getFormattedObjects } from '../../../helpers/utils' |
5 | import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../../initializers' | 5 | import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../../initializers' |
6 | import { sendUpdateActor } from '../../../lib/activitypub/send' | 6 | import { sendUpdateActor } from '../../../lib/activitypub/send' |
7 | import { | 7 | import { |
8 | asyncMiddleware, | 8 | asyncMiddleware, |
9 | asyncRetryTransactionMiddleware, | 9 | asyncRetryTransactionMiddleware, |
10 | authenticate, | 10 | authenticate, |
11 | commonVideosFiltersValidator, | ||
12 | paginationValidator, | 11 | paginationValidator, |
13 | setDefaultPagination, | 12 | setDefaultPagination, |
14 | setDefaultSort, | 13 | setDefaultSort, |
15 | userSubscriptionAddValidator, | ||
16 | userSubscriptionGetValidator, | ||
17 | usersUpdateMeValidator, | 14 | usersUpdateMeValidator, |
18 | usersVideoRatingValidator | 15 | usersVideoRatingValidator |
19 | } from '../../../middlewares' | 16 | } from '../../../middlewares' |
20 | import { | 17 | import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' |
21 | areSubscriptionsExistValidator, | ||
22 | deleteMeValidator, | ||
23 | userSubscriptionsSortValidator, | ||
24 | videoImportsSortValidator, | ||
25 | videosSortValidator | ||
26 | } from '../../../middlewares/validators' | ||
27 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 18 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
28 | import { UserModel } from '../../../models/account/user' | 19 | import { UserModel } from '../../../models/account/user' |
29 | import { VideoModel } from '../../../models/video/video' | 20 | import { VideoModel } from '../../../models/video/video' |
30 | import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' | 21 | import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' |
31 | import { buildNSFWFilter, createReqFiles } from '../../../helpers/express-utils' | 22 | import { createReqFiles } from '../../../helpers/express-utils' |
32 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' | 23 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' |
33 | import { updateAvatarValidator } from '../../../middlewares/validators/avatar' | 24 | import { updateAvatarValidator } from '../../../middlewares/validators/avatar' |
34 | import { updateActorAvatarFile } from '../../../lib/avatar' | 25 | import { updateActorAvatarFile } from '../../../lib/avatar' |
35 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' | 26 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' |
36 | import { VideoImportModel } from '../../../models/video/video-import' | 27 | import { VideoImportModel } from '../../../models/video/video-import' |
37 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | ||
38 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
39 | import { JobQueue } from '../../../lib/job-queue' | ||
40 | import { logger } from '../../../helpers/logger' | ||
41 | import { AccountModel } from '../../../models/account/account' | 28 | import { AccountModel } from '../../../models/account/account' |
42 | 29 | ||
43 | const auditLogger = auditLoggerFactory('users-me') | 30 | const auditLogger = auditLoggerFactory('users-me') |
44 | 31 | ||
45 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) | 32 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) |
46 | 33 | ||
47 | const meRouter = express.Router() | 34 | const meRouter = express.Router() |
48 | 35 | ||
@@ -98,51 +85,6 @@ meRouter.post('/me/avatar/pick', | |||
98 | asyncRetryTransactionMiddleware(updateMyAvatar) | 85 | asyncRetryTransactionMiddleware(updateMyAvatar) |
99 | ) | 86 | ) |
100 | 87 | ||
101 | // ##### Subscriptions part ##### | ||
102 | |||
103 | meRouter.get('/me/subscriptions/videos', | ||
104 | authenticate, | ||
105 | paginationValidator, | ||
106 | videosSortValidator, | ||
107 | setDefaultSort, | ||
108 | setDefaultPagination, | ||
109 | commonVideosFiltersValidator, | ||
110 | asyncMiddleware(getUserSubscriptionVideos) | ||
111 | ) | ||
112 | |||
113 | meRouter.get('/me/subscriptions/exist', | ||
114 | authenticate, | ||
115 | areSubscriptionsExistValidator, | ||
116 | asyncMiddleware(areSubscriptionsExist) | ||
117 | ) | ||
118 | |||
119 | meRouter.get('/me/subscriptions', | ||
120 | authenticate, | ||
121 | paginationValidator, | ||
122 | userSubscriptionsSortValidator, | ||
123 | setDefaultSort, | ||
124 | setDefaultPagination, | ||
125 | asyncMiddleware(getUserSubscriptions) | ||
126 | ) | ||
127 | |||
128 | meRouter.post('/me/subscriptions', | ||
129 | authenticate, | ||
130 | userSubscriptionAddValidator, | ||
131 | asyncMiddleware(addUserSubscription) | ||
132 | ) | ||
133 | |||
134 | meRouter.get('/me/subscriptions/:uri', | ||
135 | authenticate, | ||
136 | userSubscriptionGetValidator, | ||
137 | getUserSubscription | ||
138 | ) | ||
139 | |||
140 | meRouter.delete('/me/subscriptions/:uri', | ||
141 | authenticate, | ||
142 | userSubscriptionGetValidator, | ||
143 | asyncRetryTransactionMiddleware(deleteUserSubscription) | ||
144 | ) | ||
145 | |||
146 | // --------------------------------------------------------------------------- | 88 | // --------------------------------------------------------------------------- |
147 | 89 | ||
148 | export { | 90 | export { |
@@ -151,99 +93,6 @@ export { | |||
151 | 93 | ||
152 | // --------------------------------------------------------------------------- | 94 | // --------------------------------------------------------------------------- |
153 | 95 | ||
154 | async function areSubscriptionsExist (req: express.Request, res: express.Response) { | ||
155 | const uris = req.query.uris as string[] | ||
156 | const user = res.locals.oauth.token.User as UserModel | ||
157 | |||
158 | const handles = uris.map(u => { | ||
159 | let [ name, host ] = u.split('@') | ||
160 | if (host === CONFIG.WEBSERVER.HOST) host = null | ||
161 | |||
162 | return { name, host, uri: u } | ||
163 | }) | ||
164 | |||
165 | const results = await ActorFollowModel.listSubscribedIn(user.Account.Actor.id, handles) | ||
166 | |||
167 | const existObject: { [id: string ]: boolean } = {} | ||
168 | for (const handle of handles) { | ||
169 | const obj = results.find(r => { | ||
170 | const server = r.ActorFollowing.Server | ||
171 | |||
172 | return r.ActorFollowing.preferredUsername === handle.name && | ||
173 | ( | ||
174 | (!server && !handle.host) || | ||
175 | (server.host === handle.host) | ||
176 | ) | ||
177 | }) | ||
178 | |||
179 | existObject[handle.uri] = obj !== undefined | ||
180 | } | ||
181 | |||
182 | return res.json(existObject) | ||
183 | } | ||
184 | |||
185 | async function addUserSubscription (req: express.Request, res: express.Response) { | ||
186 | const user = res.locals.oauth.token.User as UserModel | ||
187 | const [ name, host ] = req.body.uri.split('@') | ||
188 | |||
189 | const payload = { | ||
190 | name, | ||
191 | host, | ||
192 | followerActorId: user.Account.Actor.id | ||
193 | } | ||
194 | |||
195 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) | ||
196 | .catch(err => logger.error('Cannot create follow job for subscription %s.', req.body.uri, err)) | ||
197 | |||
198 | return res.status(204).end() | ||
199 | } | ||
200 | |||
201 | function getUserSubscription (req: express.Request, res: express.Response) { | ||
202 | const subscription: ActorFollowModel = res.locals.subscription | ||
203 | |||
204 | return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON()) | ||
205 | } | ||
206 | |||
207 | async function deleteUserSubscription (req: express.Request, res: express.Response) { | ||
208 | const subscription: ActorFollowModel = res.locals.subscription | ||
209 | |||
210 | await sequelizeTypescript.transaction(async t => { | ||
211 | return subscription.destroy({ transaction: t }) | ||
212 | }) | ||
213 | |||
214 | return res.type('json').status(204).end() | ||
215 | } | ||
216 | |||
217 | async function getUserSubscriptions (req: express.Request, res: express.Response) { | ||
218 | const user = res.locals.oauth.token.User as UserModel | ||
219 | const actorId = user.Account.Actor.id | ||
220 | |||
221 | const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort) | ||
222 | |||
223 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
224 | } | ||
225 | |||
226 | async function getUserSubscriptionVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
227 | const user = res.locals.oauth.token.User as UserModel | ||
228 | const resultList = await VideoModel.listForApi({ | ||
229 | start: req.query.start, | ||
230 | count: req.query.count, | ||
231 | sort: req.query.sort, | ||
232 | includeLocalVideos: false, | ||
233 | categoryOneOf: req.query.categoryOneOf, | ||
234 | licenceOneOf: req.query.licenceOneOf, | ||
235 | languageOneOf: req.query.languageOneOf, | ||
236 | tagsOneOf: req.query.tagsOneOf, | ||
237 | tagsAllOf: req.query.tagsAllOf, | ||
238 | nsfw: buildNSFWFilter(res, req.query.nsfw), | ||
239 | filter: req.query.filter as VideoFilter, | ||
240 | withFiles: false, | ||
241 | actorId: user.Account.Actor.id | ||
242 | }) | ||
243 | |||
244 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
245 | } | ||
246 | |||
247 | async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 96 | async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
248 | const user = res.locals.oauth.token.User as UserModel | 97 | const user = res.locals.oauth.token.User as UserModel |
249 | const resultList = await VideoModel.listUserVideosForApi( | 98 | const resultList = await VideoModel.listUserVideosForApi( |
@@ -318,7 +167,7 @@ async function deleteMe (req: express.Request, res: express.Response) { | |||
318 | return res.sendStatus(204) | 167 | return res.sendStatus(204) |
319 | } | 168 | } |
320 | 169 | ||
321 | async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { | 170 | async function updateMe (req: express.Request, res: express.Response) { |
322 | const body: UserUpdateMe = req.body | 171 | const body: UserUpdateMe = req.body |
323 | 172 | ||
324 | const user: UserModel = res.locals.oauth.token.user | 173 | const user: UserModel = res.locals.oauth.token.user |
@@ -327,7 +176,9 @@ async function updateMe (req: express.Request, res: express.Response, next: expr | |||
327 | if (body.password !== undefined) user.password = body.password | 176 | if (body.password !== undefined) user.password = body.password |
328 | if (body.email !== undefined) user.email = body.email | 177 | if (body.email !== undefined) user.email = body.email |
329 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy | 178 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy |
179 | if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled | ||
330 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo | 180 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo |
181 | if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled | ||
331 | 182 | ||
332 | await sequelizeTypescript.transaction(async t => { | 183 | await sequelizeTypescript.transaction(async t => { |
333 | const userAccount = await AccountModel.load(user.Account.id) | 184 | const userAccount = await AccountModel.load(user.Account.id) |
@@ -346,7 +197,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr | |||
346 | return res.sendStatus(204) | 197 | return res.sendStatus(204) |
347 | } | 198 | } |
348 | 199 | ||
349 | async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { | 200 | async function updateMyAvatar (req: express.Request, res: express.Response) { |
350 | const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] | 201 | const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] |
351 | const user: UserModel = res.locals.oauth.token.user | 202 | const user: UserModel = res.locals.oauth.token.user |
352 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) | 203 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) |
diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts new file mode 100644 index 000000000..9575eab46 --- /dev/null +++ b/server/controllers/api/users/my-blocklist.ts | |||
@@ -0,0 +1,125 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | ||
3 | import { getFormattedObjects } from '../../../helpers/utils' | ||
4 | import { | ||
5 | asyncMiddleware, | ||
6 | asyncRetryTransactionMiddleware, | ||
7 | authenticate, | ||
8 | paginationValidator, | ||
9 | setDefaultPagination, | ||
10 | setDefaultSort, | ||
11 | unblockAccountByAccountValidator | ||
12 | } from '../../../middlewares' | ||
13 | import { | ||
14 | accountsBlocklistSortValidator, | ||
15 | blockAccountValidator, | ||
16 | blockServerValidator, | ||
17 | serversBlocklistSortValidator, | ||
18 | unblockServerByAccountValidator | ||
19 | } from '../../../middlewares/validators' | ||
20 | import { UserModel } from '../../../models/account/user' | ||
21 | import { AccountModel } from '../../../models/account/account' | ||
22 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' | ||
23 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' | ||
24 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' | ||
25 | import { ServerModel } from '../../../models/server/server' | ||
26 | |||
27 | const myBlocklistRouter = express.Router() | ||
28 | |||
29 | myBlocklistRouter.get('/me/blocklist/accounts', | ||
30 | authenticate, | ||
31 | paginationValidator, | ||
32 | accountsBlocklistSortValidator, | ||
33 | setDefaultSort, | ||
34 | setDefaultPagination, | ||
35 | asyncMiddleware(listBlockedAccounts) | ||
36 | ) | ||
37 | |||
38 | myBlocklistRouter.post('/me/blocklist/accounts', | ||
39 | authenticate, | ||
40 | asyncMiddleware(blockAccountValidator), | ||
41 | asyncRetryTransactionMiddleware(blockAccount) | ||
42 | ) | ||
43 | |||
44 | myBlocklistRouter.delete('/me/blocklist/accounts/:accountName', | ||
45 | authenticate, | ||
46 | asyncMiddleware(unblockAccountByAccountValidator), | ||
47 | asyncRetryTransactionMiddleware(unblockAccount) | ||
48 | ) | ||
49 | |||
50 | myBlocklistRouter.get('/me/blocklist/servers', | ||
51 | authenticate, | ||
52 | paginationValidator, | ||
53 | serversBlocklistSortValidator, | ||
54 | setDefaultSort, | ||
55 | setDefaultPagination, | ||
56 | asyncMiddleware(listBlockedServers) | ||
57 | ) | ||
58 | |||
59 | myBlocklistRouter.post('/me/blocklist/servers', | ||
60 | authenticate, | ||
61 | asyncMiddleware(blockServerValidator), | ||
62 | asyncRetryTransactionMiddleware(blockServer) | ||
63 | ) | ||
64 | |||
65 | myBlocklistRouter.delete('/me/blocklist/servers/:host', | ||
66 | authenticate, | ||
67 | asyncMiddleware(unblockServerByAccountValidator), | ||
68 | asyncRetryTransactionMiddleware(unblockServer) | ||
69 | ) | ||
70 | |||
71 | export { | ||
72 | myBlocklistRouter | ||
73 | } | ||
74 | |||
75 | // --------------------------------------------------------------------------- | ||
76 | |||
77 | async function listBlockedAccounts (req: express.Request, res: express.Response) { | ||
78 | const user: UserModel = res.locals.oauth.token.User | ||
79 | |||
80 | const resultList = await AccountBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort) | ||
81 | |||
82 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
83 | } | ||
84 | |||
85 | async function blockAccount (req: express.Request, res: express.Response) { | ||
86 | const user: UserModel = res.locals.oauth.token.User | ||
87 | const accountToBlock: AccountModel = res.locals.account | ||
88 | |||
89 | await addAccountInBlocklist(user.Account.id, accountToBlock.id) | ||
90 | |||
91 | return res.status(204).end() | ||
92 | } | ||
93 | |||
94 | async function unblockAccount (req: express.Request, res: express.Response) { | ||
95 | const accountBlock: AccountBlocklistModel = res.locals.accountBlock | ||
96 | |||
97 | await removeAccountFromBlocklist(accountBlock) | ||
98 | |||
99 | return res.status(204).end() | ||
100 | } | ||
101 | |||
102 | async function listBlockedServers (req: express.Request, res: express.Response) { | ||
103 | const user: UserModel = res.locals.oauth.token.User | ||
104 | |||
105 | const resultList = await ServerBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort) | ||
106 | |||
107 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
108 | } | ||
109 | |||
110 | async function blockServer (req: express.Request, res: express.Response) { | ||
111 | const user: UserModel = res.locals.oauth.token.User | ||
112 | const serverToBlock: ServerModel = res.locals.server | ||
113 | |||
114 | await addServerInBlocklist(user.Account.id, serverToBlock.id) | ||
115 | |||
116 | return res.status(204).end() | ||
117 | } | ||
118 | |||
119 | async function unblockServer (req: express.Request, res: express.Response) { | ||
120 | const serverBlock: ServerBlocklistModel = res.locals.serverBlock | ||
121 | |||
122 | await removeServerFromBlocklist(serverBlock) | ||
123 | |||
124 | return res.status(204).end() | ||
125 | } | ||
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts new file mode 100644 index 000000000..6cd782c47 --- /dev/null +++ b/server/controllers/api/users/my-history.ts | |||
@@ -0,0 +1,57 @@ | |||
1 | import * as express from 'express' | ||
2 | import { | ||
3 | asyncMiddleware, | ||
4 | asyncRetryTransactionMiddleware, | ||
5 | authenticate, | ||
6 | paginationValidator, | ||
7 | setDefaultPagination, | ||
8 | userHistoryRemoveValidator | ||
9 | } from '../../../middlewares' | ||
10 | import { UserModel } from '../../../models/account/user' | ||
11 | import { getFormattedObjects } from '../../../helpers/utils' | ||
12 | import { UserVideoHistoryModel } from '../../../models/account/user-video-history' | ||
13 | import { sequelizeTypescript } from '../../../initializers' | ||
14 | |||
15 | const myVideosHistoryRouter = express.Router() | ||
16 | |||
17 | myVideosHistoryRouter.get('/me/history/videos', | ||
18 | authenticate, | ||
19 | paginationValidator, | ||
20 | setDefaultPagination, | ||
21 | asyncMiddleware(listMyVideosHistory) | ||
22 | ) | ||
23 | |||
24 | myVideosHistoryRouter.post('/me/history/videos/remove', | ||
25 | authenticate, | ||
26 | userHistoryRemoveValidator, | ||
27 | asyncRetryTransactionMiddleware(removeUserHistory) | ||
28 | ) | ||
29 | |||
30 | // --------------------------------------------------------------------------- | ||
31 | |||
32 | export { | ||
33 | myVideosHistoryRouter | ||
34 | } | ||
35 | |||
36 | // --------------------------------------------------------------------------- | ||
37 | |||
38 | async function listMyVideosHistory (req: express.Request, res: express.Response) { | ||
39 | const user: UserModel = res.locals.oauth.token.User | ||
40 | |||
41 | const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count) | ||
42 | |||
43 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
44 | } | ||
45 | |||
46 | async function removeUserHistory (req: express.Request, res: express.Response) { | ||
47 | const user: UserModel = res.locals.oauth.token.User | ||
48 | const beforeDate = req.body.beforeDate || null | ||
49 | |||
50 | await sequelizeTypescript.transaction(t => { | ||
51 | return UserVideoHistoryModel.removeHistoryBefore(user, beforeDate, t) | ||
52 | }) | ||
53 | |||
54 | // Do not send the delete to other instances, we delete OUR copy of this video abuse | ||
55 | |||
56 | return res.type('json').status(204).end() | ||
57 | } | ||
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts new file mode 100644 index 000000000..76cf97587 --- /dev/null +++ b/server/controllers/api/users/my-notifications.ts | |||
@@ -0,0 +1,108 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | ||
3 | import { | ||
4 | asyncMiddleware, | ||
5 | asyncRetryTransactionMiddleware, | ||
6 | authenticate, | ||
7 | paginationValidator, | ||
8 | setDefaultPagination, | ||
9 | setDefaultSort, | ||
10 | userNotificationsSortValidator | ||
11 | } from '../../../middlewares' | ||
12 | import { UserModel } from '../../../models/account/user' | ||
13 | import { getFormattedObjects } from '../../../helpers/utils' | ||
14 | import { UserNotificationModel } from '../../../models/account/user-notification' | ||
15 | import { meRouter } from './me' | ||
16 | import { | ||
17 | listUserNotificationsValidator, | ||
18 | markAsReadUserNotificationsValidator, | ||
19 | updateNotificationSettingsValidator | ||
20 | } from '../../../middlewares/validators/user-notifications' | ||
21 | import { UserNotificationSetting } from '../../../../shared/models/users' | ||
22 | import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting' | ||
23 | |||
24 | const myNotificationsRouter = express.Router() | ||
25 | |||
26 | meRouter.put('/me/notification-settings', | ||
27 | authenticate, | ||
28 | updateNotificationSettingsValidator, | ||
29 | asyncRetryTransactionMiddleware(updateNotificationSettings) | ||
30 | ) | ||
31 | |||
32 | myNotificationsRouter.get('/me/notifications', | ||
33 | authenticate, | ||
34 | paginationValidator, | ||
35 | userNotificationsSortValidator, | ||
36 | setDefaultSort, | ||
37 | setDefaultPagination, | ||
38 | listUserNotificationsValidator, | ||
39 | asyncMiddleware(listUserNotifications) | ||
40 | ) | ||
41 | |||
42 | myNotificationsRouter.post('/me/notifications/read', | ||
43 | authenticate, | ||
44 | markAsReadUserNotificationsValidator, | ||
45 | asyncMiddleware(markAsReadUserNotifications) | ||
46 | ) | ||
47 | |||
48 | myNotificationsRouter.post('/me/notifications/read-all', | ||
49 | authenticate, | ||
50 | asyncMiddleware(markAsReadAllUserNotifications) | ||
51 | ) | ||
52 | |||
53 | export { | ||
54 | myNotificationsRouter | ||
55 | } | ||
56 | |||
57 | // --------------------------------------------------------------------------- | ||
58 | |||
59 | async function updateNotificationSettings (req: express.Request, res: express.Response) { | ||
60 | const user: UserModel = res.locals.oauth.token.User | ||
61 | const body = req.body | ||
62 | |||
63 | const query = { | ||
64 | where: { | ||
65 | userId: user.id | ||
66 | } | ||
67 | } | ||
68 | |||
69 | const values: UserNotificationSetting = { | ||
70 | newVideoFromSubscription: body.newVideoFromSubscription, | ||
71 | newCommentOnMyVideo: body.newCommentOnMyVideo, | ||
72 | videoAbuseAsModerator: body.videoAbuseAsModerator, | ||
73 | blacklistOnMyVideo: body.blacklistOnMyVideo, | ||
74 | myVideoPublished: body.myVideoPublished, | ||
75 | myVideoImportFinished: body.myVideoImportFinished, | ||
76 | newFollow: body.newFollow, | ||
77 | newUserRegistration: body.newUserRegistration, | ||
78 | commentMention: body.commentMention | ||
79 | } | ||
80 | |||
81 | await UserNotificationSettingModel.update(values, query) | ||
82 | |||
83 | return res.status(204).end() | ||
84 | } | ||
85 | |||
86 | async function listUserNotifications (req: express.Request, res: express.Response) { | ||
87 | const user: UserModel = res.locals.oauth.token.User | ||
88 | |||
89 | const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort, req.query.unread) | ||
90 | |||
91 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
92 | } | ||
93 | |||
94 | async function markAsReadUserNotifications (req: express.Request, res: express.Response) { | ||
95 | const user: UserModel = res.locals.oauth.token.User | ||
96 | |||
97 | await UserNotificationModel.markAsRead(user.id, req.body.ids) | ||
98 | |||
99 | return res.status(204).end() | ||
100 | } | ||
101 | |||
102 | async function markAsReadAllUserNotifications (req: express.Request, res: express.Response) { | ||
103 | const user: UserModel = res.locals.oauth.token.User | ||
104 | |||
105 | await UserNotificationModel.markAllAsRead(user.id) | ||
106 | |||
107 | return res.status(204).end() | ||
108 | } | ||
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts new file mode 100644 index 000000000..accca6d52 --- /dev/null +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -0,0 +1,170 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | ||
3 | import { getFormattedObjects } from '../../../helpers/utils' | ||
4 | import { CONFIG, sequelizeTypescript } from '../../../initializers' | ||
5 | import { | ||
6 | asyncMiddleware, | ||
7 | asyncRetryTransactionMiddleware, | ||
8 | authenticate, | ||
9 | commonVideosFiltersValidator, | ||
10 | paginationValidator, | ||
11 | setDefaultPagination, | ||
12 | setDefaultSort, | ||
13 | userSubscriptionAddValidator, | ||
14 | userSubscriptionGetValidator | ||
15 | } from '../../../middlewares' | ||
16 | import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators' | ||
17 | import { UserModel } from '../../../models/account/user' | ||
18 | import { VideoModel } from '../../../models/video/video' | ||
19 | import { buildNSFWFilter } from '../../../helpers/express-utils' | ||
20 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | ||
21 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
22 | import { JobQueue } from '../../../lib/job-queue' | ||
23 | import { logger } from '../../../helpers/logger' | ||
24 | |||
25 | const mySubscriptionsRouter = express.Router() | ||
26 | |||
27 | mySubscriptionsRouter.get('/me/subscriptions/videos', | ||
28 | authenticate, | ||
29 | paginationValidator, | ||
30 | videosSortValidator, | ||
31 | setDefaultSort, | ||
32 | setDefaultPagination, | ||
33 | commonVideosFiltersValidator, | ||
34 | asyncMiddleware(getUserSubscriptionVideos) | ||
35 | ) | ||
36 | |||
37 | mySubscriptionsRouter.get('/me/subscriptions/exist', | ||
38 | authenticate, | ||
39 | areSubscriptionsExistValidator, | ||
40 | asyncMiddleware(areSubscriptionsExist) | ||
41 | ) | ||
42 | |||
43 | mySubscriptionsRouter.get('/me/subscriptions', | ||
44 | authenticate, | ||
45 | paginationValidator, | ||
46 | userSubscriptionsSortValidator, | ||
47 | setDefaultSort, | ||
48 | setDefaultPagination, | ||
49 | asyncMiddleware(getUserSubscriptions) | ||
50 | ) | ||
51 | |||
52 | mySubscriptionsRouter.post('/me/subscriptions', | ||
53 | authenticate, | ||
54 | userSubscriptionAddValidator, | ||
55 | asyncMiddleware(addUserSubscription) | ||
56 | ) | ||
57 | |||
58 | mySubscriptionsRouter.get('/me/subscriptions/:uri', | ||
59 | authenticate, | ||
60 | userSubscriptionGetValidator, | ||
61 | getUserSubscription | ||
62 | ) | ||
63 | |||
64 | mySubscriptionsRouter.delete('/me/subscriptions/:uri', | ||
65 | authenticate, | ||
66 | userSubscriptionGetValidator, | ||
67 | asyncRetryTransactionMiddleware(deleteUserSubscription) | ||
68 | ) | ||
69 | |||
70 | // --------------------------------------------------------------------------- | ||
71 | |||
72 | export { | ||
73 | mySubscriptionsRouter | ||
74 | } | ||
75 | |||
76 | // --------------------------------------------------------------------------- | ||
77 | |||
78 | async function areSubscriptionsExist (req: express.Request, res: express.Response) { | ||
79 | const uris = req.query.uris as string[] | ||
80 | const user = res.locals.oauth.token.User as UserModel | ||
81 | |||
82 | const handles = uris.map(u => { | ||
83 | let [ name, host ] = u.split('@') | ||
84 | if (host === CONFIG.WEBSERVER.HOST) host = null | ||
85 | |||
86 | return { name, host, uri: u } | ||
87 | }) | ||
88 | |||
89 | const results = await ActorFollowModel.listSubscribedIn(user.Account.Actor.id, handles) | ||
90 | |||
91 | const existObject: { [id: string ]: boolean } = {} | ||
92 | for (const handle of handles) { | ||
93 | const obj = results.find(r => { | ||
94 | const server = r.ActorFollowing.Server | ||
95 | |||
96 | return r.ActorFollowing.preferredUsername === handle.name && | ||
97 | ( | ||
98 | (!server && !handle.host) || | ||
99 | (server.host === handle.host) | ||
100 | ) | ||
101 | }) | ||
102 | |||
103 | existObject[handle.uri] = obj !== undefined | ||
104 | } | ||
105 | |||
106 | return res.json(existObject) | ||
107 | } | ||
108 | |||
109 | async function addUserSubscription (req: express.Request, res: express.Response) { | ||
110 | const user = res.locals.oauth.token.User as UserModel | ||
111 | const [ name, host ] = req.body.uri.split('@') | ||
112 | |||
113 | const payload = { | ||
114 | name, | ||
115 | host, | ||
116 | followerActorId: user.Account.Actor.id | ||
117 | } | ||
118 | |||
119 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) | ||
120 | .catch(err => logger.error('Cannot create follow job for subscription %s.', req.body.uri, err)) | ||
121 | |||
122 | return res.status(204).end() | ||
123 | } | ||
124 | |||
125 | function getUserSubscription (req: express.Request, res: express.Response) { | ||
126 | const subscription: ActorFollowModel = res.locals.subscription | ||
127 | |||
128 | return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON()) | ||
129 | } | ||
130 | |||
131 | async function deleteUserSubscription (req: express.Request, res: express.Response) { | ||
132 | const subscription: ActorFollowModel = res.locals.subscription | ||
133 | |||
134 | await sequelizeTypescript.transaction(async t => { | ||
135 | return subscription.destroy({ transaction: t }) | ||
136 | }) | ||
137 | |||
138 | return res.type('json').status(204).end() | ||
139 | } | ||
140 | |||
141 | async function getUserSubscriptions (req: express.Request, res: express.Response) { | ||
142 | const user = res.locals.oauth.token.User as UserModel | ||
143 | const actorId = user.Account.Actor.id | ||
144 | |||
145 | const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort) | ||
146 | |||
147 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
148 | } | ||
149 | |||
150 | async function getUserSubscriptionVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
151 | const user = res.locals.oauth.token.User as UserModel | ||
152 | const resultList = await VideoModel.listForApi({ | ||
153 | start: req.query.start, | ||
154 | count: req.query.count, | ||
155 | sort: req.query.sort, | ||
156 | includeLocalVideos: false, | ||
157 | categoryOneOf: req.query.categoryOneOf, | ||
158 | licenceOneOf: req.query.licenceOneOf, | ||
159 | languageOneOf: req.query.languageOneOf, | ||
160 | tagsOneOf: req.query.tagsOneOf, | ||
161 | tagsAllOf: req.query.tagsAllOf, | ||
162 | nsfw: buildNSFWFilter(res, req.query.nsfw), | ||
163 | filter: req.query.filter as VideoFilter, | ||
164 | withFiles: false, | ||
165 | followerActorId: user.Account.Actor.id, | ||
166 | user | ||
167 | }) | ||
168 | |||
169 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
170 | } | ||
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 1fa842d9c..db7602139 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -22,7 +22,7 @@ import { createVideoChannel } from '../../lib/video-channel' | |||
22 | import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 22 | import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
23 | import { setAsyncActorKeys } from '../../lib/activitypub' | 23 | import { setAsyncActorKeys } from '../../lib/activitypub' |
24 | import { AccountModel } from '../../models/account/account' | 24 | import { AccountModel } from '../../models/account/account' |
25 | import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' | 25 | import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers' |
26 | import { logger } from '../../helpers/logger' | 26 | import { logger } from '../../helpers/logger' |
27 | import { VideoModel } from '../../models/video/video' | 27 | import { VideoModel } from '../../models/video/video' |
28 | import { updateAvatarValidator } from '../../middlewares/validators/avatar' | 28 | import { updateAvatarValidator } from '../../middlewares/validators/avatar' |
@@ -30,9 +30,10 @@ import { updateActorAvatarFile } from '../../lib/avatar' | |||
30 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' | 30 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' |
31 | import { resetSequelizeInstance } from '../../helpers/database-utils' | 31 | import { resetSequelizeInstance } from '../../helpers/database-utils' |
32 | import { UserModel } from '../../models/account/user' | 32 | import { UserModel } from '../../models/account/user' |
33 | import { JobQueue } from '../../lib/job-queue' | ||
33 | 34 | ||
34 | const auditLogger = auditLoggerFactory('channels') | 35 | const auditLogger = auditLoggerFactory('channels') |
35 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) | 36 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) |
36 | 37 | ||
37 | const videoChannelRouter = express.Router() | 38 | const videoChannelRouter = express.Router() |
38 | 39 | ||
@@ -197,15 +198,20 @@ async function removeVideoChannel (req: express.Request, res: express.Response) | |||
197 | async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { | 198 | async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { |
198 | const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) | 199 | const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) |
199 | 200 | ||
201 | if (videoChannelWithVideos.isOutdated()) { | ||
202 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } }) | ||
203 | .catch(err => logger.error('Cannot create AP refresher job for actor %s.', videoChannelWithVideos.Actor.url, { err })) | ||
204 | } | ||
205 | |||
200 | return res.json(videoChannelWithVideos.toFormattedJSON()) | 206 | return res.json(videoChannelWithVideos.toFormattedJSON()) |
201 | } | 207 | } |
202 | 208 | ||
203 | async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 209 | async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
204 | const videoChannelInstance: VideoChannelModel = res.locals.videoChannel | 210 | const videoChannelInstance: VideoChannelModel = res.locals.videoChannel |
205 | const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | 211 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined |
206 | 212 | ||
207 | const resultList = await VideoModel.listForApi({ | 213 | const resultList = await VideoModel.listForApi({ |
208 | actorId, | 214 | followerActorId, |
209 | start: req.query.start, | 215 | start: req.query.start, |
210 | count: req.query.count, | 216 | count: req.query.count, |
211 | sort: req.query.sort, | 217 | sort: req.query.sort, |
@@ -215,9 +221,11 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon | |||
215 | languageOneOf: req.query.languageOneOf, | 221 | languageOneOf: req.query.languageOneOf, |
216 | tagsOneOf: req.query.tagsOneOf, | 222 | tagsOneOf: req.query.tagsOneOf, |
217 | tagsAllOf: req.query.tagsAllOf, | 223 | tagsAllOf: req.query.tagsAllOf, |
224 | filter: req.query.filter, | ||
218 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 225 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
219 | withFiles: false, | 226 | withFiles: false, |
220 | videoChannelId: videoChannelInstance.id | 227 | videoChannelId: videoChannelInstance.id, |
228 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
221 | }) | 229 | }) |
222 | 230 | ||
223 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 231 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index d0c81804b..32f9c4793 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts | |||
@@ -3,7 +3,6 @@ import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared | |||
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { getFormattedObjects } from '../../../helpers/utils' | 4 | import { getFormattedObjects } from '../../../helpers/utils' |
5 | import { sequelizeTypescript } from '../../../initializers' | 5 | import { sequelizeTypescript } from '../../../initializers' |
6 | import { sendVideoAbuse } from '../../../lib/activitypub/send' | ||
7 | import { | 6 | import { |
8 | asyncMiddleware, | 7 | asyncMiddleware, |
9 | asyncRetryTransactionMiddleware, | 8 | asyncRetryTransactionMiddleware, |
@@ -22,6 +21,8 @@ import { VideoModel } from '../../../models/video/video' | |||
22 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | 21 | import { VideoAbuseModel } from '../../../models/video/video-abuse' |
23 | import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' | 22 | import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' |
24 | import { UserModel } from '../../../models/account/user' | 23 | import { UserModel } from '../../../models/account/user' |
24 | import { Notifier } from '../../../lib/notifier' | ||
25 | import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag' | ||
25 | 26 | ||
26 | const auditLogger = auditLoggerFactory('abuse') | 27 | const auditLogger = auditLoggerFactory('abuse') |
27 | const abuseVideoRouter = express.Router() | 28 | const abuseVideoRouter = express.Router() |
@@ -117,6 +118,8 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) { | |||
117 | await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance) | 118 | await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance) |
118 | } | 119 | } |
119 | 120 | ||
121 | Notifier.Instance.notifyOnNewVideoAbuse(videoAbuseInstance) | ||
122 | |||
120 | auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) | 123 | auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) |
121 | 124 | ||
122 | return videoAbuseInstance | 125 | return videoAbuseInstance |
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts index 7f803c8e9..43b0516e7 100644 --- a/server/controllers/api/videos/blacklist.ts +++ b/server/controllers/api/videos/blacklist.ts | |||
@@ -16,6 +16,10 @@ import { | |||
16 | } from '../../../middlewares' | 16 | } from '../../../middlewares' |
17 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | 17 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' |
18 | import { sequelizeTypescript } from '../../../initializers' | 18 | import { sequelizeTypescript } from '../../../initializers' |
19 | import { Notifier } from '../../../lib/notifier' | ||
20 | import { VideoModel } from '../../../models/video/video' | ||
21 | import { sendCreateVideo, sendDeleteVideo, sendUpdateVideo } from '../../../lib/activitypub/send' | ||
22 | import { federateVideoIfNeeded } from '../../../lib/activitypub' | ||
19 | 23 | ||
20 | const blacklistRouter = express.Router() | 24 | const blacklistRouter = express.Router() |
21 | 25 | ||
@@ -64,16 +68,26 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response) | |||
64 | 68 | ||
65 | const toCreate = { | 69 | const toCreate = { |
66 | videoId: videoInstance.id, | 70 | videoId: videoInstance.id, |
71 | unfederated: body.unfederate === true, | ||
67 | reason: body.reason | 72 | reason: body.reason |
68 | } | 73 | } |
69 | 74 | ||
70 | await VideoBlacklistModel.create(toCreate) | 75 | const blacklist = await VideoBlacklistModel.create(toCreate) |
76 | blacklist.Video = videoInstance | ||
77 | |||
78 | if (body.unfederate === true) { | ||
79 | await sendDeleteVideo(videoInstance, undefined) | ||
80 | } | ||
81 | |||
82 | Notifier.Instance.notifyOnVideoBlacklist(blacklist) | ||
83 | |||
84 | logger.info('Video %s blacklisted.', res.locals.video.uuid) | ||
85 | |||
71 | return res.type('json').status(204).end() | 86 | return res.type('json').status(204).end() |
72 | } | 87 | } |
73 | 88 | ||
74 | async function updateVideoBlacklistController (req: express.Request, res: express.Response) { | 89 | async function updateVideoBlacklistController (req: express.Request, res: express.Response) { |
75 | const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel | 90 | const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel |
76 | logger.info(videoBlacklist) | ||
77 | 91 | ||
78 | if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason | 92 | if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason |
79 | 93 | ||
@@ -92,11 +106,20 @@ async function listBlacklist (req: express.Request, res: express.Response, next: | |||
92 | 106 | ||
93 | async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { | 107 | async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { |
94 | const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel | 108 | const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel |
109 | const video: VideoModel = res.locals.video | ||
95 | 110 | ||
96 | await sequelizeTypescript.transaction(t => { | 111 | await sequelizeTypescript.transaction(async t => { |
97 | return videoBlacklist.destroy({ transaction: t }) | 112 | const unfederated = videoBlacklist.unfederated |
113 | await videoBlacklist.destroy({ transaction: t }) | ||
114 | |||
115 | // Re federate the video | ||
116 | if (unfederated === true) { | ||
117 | await federateVideoIfNeeded(video, true, t) | ||
118 | } | ||
98 | }) | 119 | }) |
99 | 120 | ||
121 | Notifier.Instance.notifyOnVideoUnblacklist(video) | ||
122 | |||
100 | logger.info('Video %s removed from blacklist.', res.locals.video.uuid) | 123 | logger.info('Video %s removed from blacklist.', res.locals.video.uuid) |
101 | 124 | ||
102 | return res.type('json').status(204).end() | 125 | return res.type('json').status(204).end() |
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts index 3ba918189..9b3661368 100644 --- a/server/controllers/api/videos/captions.ts +++ b/server/controllers/api/videos/captions.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' | 2 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' |
3 | import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators' | 3 | import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators' |
4 | import { createReqFiles } from '../../../helpers/express-utils' | 4 | import { createReqFiles } from '../../../helpers/express-utils' |
5 | import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' | 5 | import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../../initializers' |
6 | import { getFormattedObjects } from '../../../helpers/utils' | 6 | import { getFormattedObjects } from '../../../helpers/utils' |
7 | import { VideoCaptionModel } from '../../../models/video/video-caption' | 7 | import { VideoCaptionModel } from '../../../models/video/video-caption' |
8 | import { VideoModel } from '../../../models/video/video' | 8 | import { VideoModel } from '../../../models/video/video' |
@@ -12,7 +12,7 @@ import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' | |||
12 | 12 | ||
13 | const reqVideoCaptionAdd = createReqFiles( | 13 | const reqVideoCaptionAdd = createReqFiles( |
14 | [ 'captionfile' ], | 14 | [ 'captionfile' ], |
15 | VIDEO_CAPTIONS_MIMETYPE_EXT, | 15 | MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT, |
16 | { | 16 | { |
17 | captionfile: CONFIG.STORAGE.CAPTIONS_DIR | 17 | captionfile: CONFIG.STORAGE.CAPTIONS_DIR |
18 | } | 18 | } |
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index 4f2b4faee..70c1148ba 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -8,7 +8,7 @@ import { buildFormattedCommentTree, createVideoComment } from '../../../lib/vide | |||
8 | import { | 8 | import { |
9 | asyncMiddleware, | 9 | asyncMiddleware, |
10 | asyncRetryTransactionMiddleware, | 10 | asyncRetryTransactionMiddleware, |
11 | authenticate, | 11 | authenticate, optionalAuthenticate, |
12 | paginationValidator, | 12 | paginationValidator, |
13 | setDefaultPagination, | 13 | setDefaultPagination, |
14 | setDefaultSort | 14 | setDefaultSort |
@@ -26,6 +26,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment' | |||
26 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' | 26 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' |
27 | import { AccountModel } from '../../../models/account/account' | 27 | import { AccountModel } from '../../../models/account/account' |
28 | import { UserModel } from '../../../models/account/user' | 28 | import { UserModel } from '../../../models/account/user' |
29 | import { Notifier } from '../../../lib/notifier' | ||
29 | 30 | ||
30 | const auditLogger = auditLoggerFactory('comments') | 31 | const auditLogger = auditLoggerFactory('comments') |
31 | const videoCommentRouter = express.Router() | 32 | const videoCommentRouter = express.Router() |
@@ -36,10 +37,12 @@ videoCommentRouter.get('/:videoId/comment-threads', | |||
36 | setDefaultSort, | 37 | setDefaultSort, |
37 | setDefaultPagination, | 38 | setDefaultPagination, |
38 | asyncMiddleware(listVideoCommentThreadsValidator), | 39 | asyncMiddleware(listVideoCommentThreadsValidator), |
40 | optionalAuthenticate, | ||
39 | asyncMiddleware(listVideoThreads) | 41 | asyncMiddleware(listVideoThreads) |
40 | ) | 42 | ) |
41 | videoCommentRouter.get('/:videoId/comment-threads/:threadId', | 43 | videoCommentRouter.get('/:videoId/comment-threads/:threadId', |
42 | asyncMiddleware(listVideoThreadCommentsValidator), | 44 | asyncMiddleware(listVideoThreadCommentsValidator), |
45 | optionalAuthenticate, | ||
43 | asyncMiddleware(listVideoThreadComments) | 46 | asyncMiddleware(listVideoThreadComments) |
44 | ) | 47 | ) |
45 | 48 | ||
@@ -69,10 +72,12 @@ export { | |||
69 | 72 | ||
70 | async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) { | 73 | async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) { |
71 | const video = res.locals.video as VideoModel | 74 | const video = res.locals.video as VideoModel |
75 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
76 | |||
72 | let resultList: ResultList<VideoCommentModel> | 77 | let resultList: ResultList<VideoCommentModel> |
73 | 78 | ||
74 | if (video.commentsEnabled === true) { | 79 | if (video.commentsEnabled === true) { |
75 | resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort) | 80 | resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort, user) |
76 | } else { | 81 | } else { |
77 | resultList = { | 82 | resultList = { |
78 | total: 0, | 83 | total: 0, |
@@ -85,10 +90,12 @@ async function listVideoThreads (req: express.Request, res: express.Response, ne | |||
85 | 90 | ||
86 | async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) { | 91 | async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) { |
87 | const video = res.locals.video as VideoModel | 92 | const video = res.locals.video as VideoModel |
93 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
94 | |||
88 | let resultList: ResultList<VideoCommentModel> | 95 | let resultList: ResultList<VideoCommentModel> |
89 | 96 | ||
90 | if (video.commentsEnabled === true) { | 97 | if (video.commentsEnabled === true) { |
91 | resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id) | 98 | resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id, user) |
92 | } else { | 99 | } else { |
93 | resultList = { | 100 | resultList = { |
94 | total: 0, | 101 | total: 0, |
@@ -113,6 +120,7 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons | |||
113 | }, t) | 120 | }, t) |
114 | }) | 121 | }) |
115 | 122 | ||
123 | Notifier.Instance.notifyOnNewComment(comment) | ||
116 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) | 124 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) |
117 | 125 | ||
118 | return res.json({ | 126 | return res.json({ |
@@ -134,6 +142,7 @@ async function addVideoCommentReply (req: express.Request, res: express.Response | |||
134 | }, t) | 142 | }, t) |
135 | }) | 143 | }) |
136 | 144 | ||
145 | Notifier.Instance.notifyOnNewComment(comment) | ||
137 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) | 146 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) |
138 | 147 | ||
139 | return res.json({ comment: comment.toFormattedJSON() }).end() | 148 | return res.json({ comment: comment.toFormattedJSON() }).end() |
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 9e51e2000..7053d5253 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -3,14 +3,7 @@ import * as magnetUtil from 'magnet-uri' | |||
3 | import 'multer' | 3 | import 'multer' |
4 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' | 4 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' |
5 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' | 5 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' |
6 | import { | 6 | import { CONFIG, MIMETYPES, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE } from '../../../initializers' |
7 | CONFIG, | ||
8 | IMAGE_MIMETYPE_EXT, | ||
9 | PREVIEWS_SIZE, | ||
10 | sequelizeTypescript, | ||
11 | THUMBNAILS_SIZE, | ||
12 | TORRENT_MIMETYPE_EXT | ||
13 | } from '../../../initializers' | ||
14 | import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' | 7 | import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' |
15 | import { createReqFiles } from '../../../helpers/express-utils' | 8 | import { createReqFiles } from '../../../helpers/express-utils' |
16 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
@@ -28,18 +21,18 @@ import { VideoChannelModel } from '../../../models/video/video-channel' | |||
28 | import * as Bluebird from 'bluebird' | 21 | import * as Bluebird from 'bluebird' |
29 | import * as parseTorrent from 'parse-torrent' | 22 | import * as parseTorrent from 'parse-torrent' |
30 | import { getSecureTorrentName } from '../../../helpers/utils' | 23 | import { getSecureTorrentName } from '../../../helpers/utils' |
31 | import { readFile, rename } from 'fs-extra' | 24 | import { readFile, move } from 'fs-extra' |
32 | 25 | ||
33 | const auditLogger = auditLoggerFactory('video-imports') | 26 | const auditLogger = auditLoggerFactory('video-imports') |
34 | const videoImportsRouter = express.Router() | 27 | const videoImportsRouter = express.Router() |
35 | 28 | ||
36 | const reqVideoFileImport = createReqFiles( | 29 | const reqVideoFileImport = createReqFiles( |
37 | [ 'thumbnailfile', 'previewfile', 'torrentfile' ], | 30 | [ 'thumbnailfile', 'previewfile', 'torrentfile' ], |
38 | Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), | 31 | Object.assign({}, MIMETYPES.TORRENT.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), |
39 | { | 32 | { |
40 | thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, | 33 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, |
41 | previewfile: CONFIG.STORAGE.PREVIEWS_DIR, | 34 | previewfile: CONFIG.STORAGE.TMP_DIR, |
42 | torrentfile: CONFIG.STORAGE.TORRENTS_DIR | 35 | torrentfile: CONFIG.STORAGE.TMP_DIR |
43 | } | 36 | } |
44 | ) | 37 | ) |
45 | 38 | ||
@@ -78,7 +71,7 @@ async function addTorrentImport (req: express.Request, res: express.Response, to | |||
78 | 71 | ||
79 | // Rename the torrent to a secured name | 72 | // Rename the torrent to a secured name |
80 | const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) | 73 | const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) |
81 | await rename(torrentfile.path, newTorrentPath) | 74 | await move(torrentfile.path, newTorrentPath) |
82 | torrentfile.path = newTorrentPath | 75 | torrentfile.path = newTorrentPath |
83 | 76 | ||
84 | const buf = await readFile(torrentfile.path) | 77 | const buf = await readFile(torrentfile.path) |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 7d55f06b6..76a318d13 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -8,14 +8,13 @@ import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../ | |||
8 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' | 8 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' |
9 | import { | 9 | import { |
10 | CONFIG, | 10 | CONFIG, |
11 | IMAGE_MIMETYPE_EXT, | 11 | MIMETYPES, |
12 | PREVIEWS_SIZE, | 12 | PREVIEWS_SIZE, |
13 | sequelizeTypescript, | 13 | sequelizeTypescript, |
14 | THUMBNAILS_SIZE, | 14 | THUMBNAILS_SIZE, |
15 | VIDEO_CATEGORIES, | 15 | VIDEO_CATEGORIES, |
16 | VIDEO_LANGUAGES, | 16 | VIDEO_LANGUAGES, |
17 | VIDEO_LICENCES, | 17 | VIDEO_LICENCES, |
18 | VIDEO_MIMETYPE_EXT, | ||
19 | VIDEO_PRIVACIES | 18 | VIDEO_PRIVACIES |
20 | } from '../../../initializers' | 19 | } from '../../../initializers' |
21 | import { | 20 | import { |
@@ -24,19 +23,20 @@ import { | |||
24 | fetchRemoteVideoDescription, | 23 | fetchRemoteVideoDescription, |
25 | getVideoActivityPubUrl | 24 | getVideoActivityPubUrl |
26 | } from '../../../lib/activitypub' | 25 | } from '../../../lib/activitypub' |
27 | import { sendCreateView } from '../../../lib/activitypub/send' | ||
28 | import { JobQueue } from '../../../lib/job-queue' | 26 | import { JobQueue } from '../../../lib/job-queue' |
29 | import { Redis } from '../../../lib/redis' | 27 | import { Redis } from '../../../lib/redis' |
30 | import { | 28 | import { |
31 | asyncMiddleware, | 29 | asyncMiddleware, |
32 | asyncRetryTransactionMiddleware, | 30 | asyncRetryTransactionMiddleware, |
33 | authenticate, | 31 | authenticate, |
32 | checkVideoFollowConstraints, | ||
34 | commonVideosFiltersValidator, | 33 | commonVideosFiltersValidator, |
35 | optionalAuthenticate, | 34 | optionalAuthenticate, |
36 | paginationValidator, | 35 | paginationValidator, |
37 | setDefaultPagination, | 36 | setDefaultPagination, |
38 | setDefaultSort, | 37 | setDefaultSort, |
39 | videosAddValidator, | 38 | videosAddValidator, |
39 | videosCustomGetValidator, | ||
40 | videosGetValidator, | 40 | videosGetValidator, |
41 | videosRemoveValidator, | 41 | videosRemoveValidator, |
42 | videosSortValidator, | 42 | videosSortValidator, |
@@ -56,27 +56,29 @@ import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-u | |||
56 | import { videoCaptionsRouter } from './captions' | 56 | import { videoCaptionsRouter } from './captions' |
57 | import { videoImportsRouter } from './import' | 57 | import { videoImportsRouter } from './import' |
58 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | 58 | import { resetSequelizeInstance } from '../../../helpers/database-utils' |
59 | import { rename } from 'fs-extra' | 59 | import { move } from 'fs-extra' |
60 | import { watchingRouter } from './watching' | 60 | import { watchingRouter } from './watching' |
61 | import { Notifier } from '../../../lib/notifier' | ||
62 | import { sendView } from '../../../lib/activitypub/send/send-view' | ||
61 | 63 | ||
62 | const auditLogger = auditLoggerFactory('videos') | 64 | const auditLogger = auditLoggerFactory('videos') |
63 | const videosRouter = express.Router() | 65 | const videosRouter = express.Router() |
64 | 66 | ||
65 | const reqVideoFileAdd = createReqFiles( | 67 | const reqVideoFileAdd = createReqFiles( |
66 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | 68 | [ 'videofile', 'thumbnailfile', 'previewfile' ], |
67 | Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), | 69 | Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), |
68 | { | 70 | { |
69 | videofile: CONFIG.STORAGE.VIDEOS_DIR, | 71 | videofile: CONFIG.STORAGE.TMP_DIR, |
70 | thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, | 72 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, |
71 | previewfile: CONFIG.STORAGE.PREVIEWS_DIR | 73 | previewfile: CONFIG.STORAGE.TMP_DIR |
72 | } | 74 | } |
73 | ) | 75 | ) |
74 | const reqVideoFileUpdate = createReqFiles( | 76 | const reqVideoFileUpdate = createReqFiles( |
75 | [ 'thumbnailfile', 'previewfile' ], | 77 | [ 'thumbnailfile', 'previewfile' ], |
76 | IMAGE_MIMETYPE_EXT, | 78 | MIMETYPES.IMAGE.MIMETYPE_EXT, |
77 | { | 79 | { |
78 | thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, | 80 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, |
79 | previewfile: CONFIG.STORAGE.PREVIEWS_DIR | 81 | previewfile: CONFIG.STORAGE.TMP_DIR |
80 | } | 82 | } |
81 | ) | 83 | ) |
82 | 84 | ||
@@ -122,8 +124,9 @@ videosRouter.get('/:id/description', | |||
122 | ) | 124 | ) |
123 | videosRouter.get('/:id', | 125 | videosRouter.get('/:id', |
124 | optionalAuthenticate, | 126 | optionalAuthenticate, |
125 | asyncMiddleware(videosGetValidator), | 127 | asyncMiddleware(videosCustomGetValidator('only-video-with-rights')), |
126 | getVideo | 128 | asyncMiddleware(checkVideoFollowConstraints), |
129 | asyncMiddleware(getVideo) | ||
127 | ) | 130 | ) |
128 | videosRouter.post('/:id/views', | 131 | videosRouter.post('/:id/views', |
129 | asyncMiddleware(videosGetValidator), | 132 | asyncMiddleware(videosGetValidator), |
@@ -207,7 +210,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
207 | // Move physical file | 210 | // Move physical file |
208 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR | 211 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR |
209 | const destination = join(videoDir, video.getVideoFilename(videoFile)) | 212 | const destination = join(videoDir, video.getVideoFilename(videoFile)) |
210 | await rename(videoPhysicalFile.path, destination) | 213 | await move(videoPhysicalFile.path, destination) |
211 | // This is important in case if there is another attempt in the retry process | 214 | // This is important in case if there is another attempt in the retry process |
212 | videoPhysicalFile.filename = video.getVideoFilename(videoFile) | 215 | videoPhysicalFile.filename = video.getVideoFilename(videoFile) |
213 | videoPhysicalFile.path = destination | 216 | videoPhysicalFile.path = destination |
@@ -270,6 +273,8 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
270 | return videoCreated | 273 | return videoCreated |
271 | }) | 274 | }) |
272 | 275 | ||
276 | Notifier.Instance.notifyOnNewVideo(videoCreated) | ||
277 | |||
273 | if (video.state === VideoState.TO_TRANSCODE) { | 278 | if (video.state === VideoState.TO_TRANSCODE) { |
274 | // Put uuid because we don't have id auto incremented for now | 279 | // Put uuid because we don't have id auto incremented for now |
275 | const dataInput = { | 280 | const dataInput = { |
@@ -294,6 +299,7 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
294 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) | 299 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) |
295 | const videoInfoToUpdate: VideoUpdate = req.body | 300 | const videoInfoToUpdate: VideoUpdate = req.body |
296 | const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE | 301 | const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE |
302 | const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED | ||
297 | 303 | ||
298 | // Process thumbnail or create it from the video | 304 | // Process thumbnail or create it from the video |
299 | if (req.files && req.files['thumbnailfile']) { | 305 | if (req.files && req.files['thumbnailfile']) { |
@@ -308,10 +314,8 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
308 | } | 314 | } |
309 | 315 | ||
310 | try { | 316 | try { |
311 | await sequelizeTypescript.transaction(async t => { | 317 | const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => { |
312 | const sequelizeOptions = { | 318 | const sequelizeOptions = { transaction: t } |
313 | transaction: t | ||
314 | } | ||
315 | const oldVideoChannel = videoInstance.VideoChannel | 319 | const oldVideoChannel = videoInstance.VideoChannel |
316 | 320 | ||
317 | if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name) | 321 | if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name) |
@@ -363,7 +367,11 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
363 | } | 367 | } |
364 | 368 | ||
365 | const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE | 369 | const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE |
366 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | 370 | |
371 | // Don't send update if the video was unfederated | ||
372 | if (!videoInstanceUpdated.VideoBlacklist || videoInstanceUpdated.VideoBlacklist.unfederated === false) { | ||
373 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | ||
374 | } | ||
367 | 375 | ||
368 | auditLogger.update( | 376 | auditLogger.update( |
369 | getAuditIdFromRes(res), | 377 | getAuditIdFromRes(res), |
@@ -371,7 +379,13 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
371 | oldVideoAuditView | 379 | oldVideoAuditView |
372 | ) | 380 | ) |
373 | logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) | 381 | logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) |
382 | |||
383 | return videoInstanceUpdated | ||
374 | }) | 384 | }) |
385 | |||
386 | if (wasUnlistedVideo || wasPrivateVideo) { | ||
387 | Notifier.Instance.notifyOnNewVideo(videoInstanceUpdated) | ||
388 | } | ||
375 | } catch (err) { | 389 | } catch (err) { |
376 | // Force fields we want to update | 390 | // Force fields we want to update |
377 | // If the transaction is retried, sequelize will think the object has not changed | 391 | // If the transaction is retried, sequelize will think the object has not changed |
@@ -384,10 +398,17 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
384 | return res.type('json').status(204).end() | 398 | return res.type('json').status(204).end() |
385 | } | 399 | } |
386 | 400 | ||
387 | function getVideo (req: express.Request, res: express.Response) { | 401 | async function getVideo (req: express.Request, res: express.Response) { |
388 | const videoInstance = res.locals.video | 402 | // We need more attributes |
403 | const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null | ||
404 | const video: VideoModel = await VideoModel.loadForGetAPI(res.locals.video.id, undefined, userId) | ||
389 | 405 | ||
390 | return res.json(videoInstance.toFormattedDetailsJSON()) | 406 | if (video.isOutdated()) { |
407 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } }) | ||
408 | .catch(err => logger.error('Cannot create AP refresher job for video %s.', video.url, { err })) | ||
409 | } | ||
410 | |||
411 | return res.json(video.toFormattedDetailsJSON()) | ||
391 | } | 412 | } |
392 | 413 | ||
393 | async function viewVideo (req: express.Request, res: express.Response) { | 414 | async function viewVideo (req: express.Request, res: express.Response) { |
@@ -406,8 +427,7 @@ async function viewVideo (req: express.Request, res: express.Response) { | |||
406 | ]) | 427 | ]) |
407 | 428 | ||
408 | const serverActor = await getServerActor() | 429 | const serverActor = await getServerActor() |
409 | 430 | await sendView(serverActor, videoInstance, undefined) | |
410 | await sendCreateView(serverActor, videoInstance, undefined) | ||
411 | 431 | ||
412 | return res.status(204).end() | 432 | return res.status(204).end() |
413 | } | 433 | } |
@@ -425,7 +445,7 @@ async function getVideoDescription (req: express.Request, res: express.Response) | |||
425 | return res.json({ description }) | 445 | return res.json({ description }) |
426 | } | 446 | } |
427 | 447 | ||
428 | async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 448 | async function listVideos (req: express.Request, res: express.Response) { |
429 | const resultList = await VideoModel.listForApi({ | 449 | const resultList = await VideoModel.listForApi({ |
430 | start: req.query.start, | 450 | start: req.query.start, |
431 | count: req.query.count, | 451 | count: req.query.count, |
@@ -439,7 +459,7 @@ async function listVideos (req: express.Request, res: express.Response, next: ex | |||
439 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 459 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
440 | filter: req.query.filter as VideoFilter, | 460 | filter: req.query.filter as VideoFilter, |
441 | withFiles: false, | 461 | withFiles: false, |
442 | userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 462 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined |
443 | }) | 463 | }) |
444 | 464 | ||
445 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 465 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts index dc322bb0c..53952a0a2 100644 --- a/server/controllers/api/videos/rate.ts +++ b/server/controllers/api/videos/rate.ts | |||
@@ -2,8 +2,8 @@ import * as express from 'express' | |||
2 | import { UserVideoRateUpdate } from '../../../../shared' | 2 | import { UserVideoRateUpdate } from '../../../../shared' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers' | 4 | import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers' |
5 | import { sendVideoRateChange } from '../../../lib/activitypub' | 5 | import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub' |
6 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoRateValidator } from '../../../middlewares' | 6 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares' |
7 | import { AccountModel } from '../../../models/account/account' | 7 | import { AccountModel } from '../../../models/account/account' |
8 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 8 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
9 | import { VideoModel } from '../../../models/video/video' | 9 | import { VideoModel } from '../../../models/video/video' |
@@ -12,7 +12,7 @@ const rateVideoRouter = express.Router() | |||
12 | 12 | ||
13 | rateVideoRouter.put('/:id/rate', | 13 | rateVideoRouter.put('/:id/rate', |
14 | authenticate, | 14 | authenticate, |
15 | asyncMiddleware(videoRateValidator), | 15 | asyncMiddleware(videoUpdateRateValidator), |
16 | asyncRetryTransactionMiddleware(rateVideo) | 16 | asyncRetryTransactionMiddleware(rateVideo) |
17 | ) | 17 | ) |
18 | 18 | ||
@@ -28,11 +28,12 @@ async function rateVideo (req: express.Request, res: express.Response) { | |||
28 | const body: UserVideoRateUpdate = req.body | 28 | const body: UserVideoRateUpdate = req.body |
29 | const rateType = body.rating | 29 | const rateType = body.rating |
30 | const videoInstance: VideoModel = res.locals.video | 30 | const videoInstance: VideoModel = res.locals.video |
31 | const userAccount: AccountModel = res.locals.oauth.token.User.Account | ||
31 | 32 | ||
32 | await sequelizeTypescript.transaction(async t => { | 33 | await sequelizeTypescript.transaction(async t => { |
33 | const sequelizeOptions = { transaction: t } | 34 | const sequelizeOptions = { transaction: t } |
34 | 35 | ||
35 | const accountInstance = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) | 36 | const accountInstance = await AccountModel.load(userAccount.id, t) |
36 | const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t) | 37 | const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t) |
37 | 38 | ||
38 | let likesToIncrement = 0 | 39 | let likesToIncrement = 0 |
@@ -44,20 +45,22 @@ async function rateVideo (req: express.Request, res: express.Response) { | |||
44 | // There was a previous rate, update it | 45 | // There was a previous rate, update it |
45 | if (previousRate) { | 46 | if (previousRate) { |
46 | // We will remove the previous rate, so we will need to update the video count attribute | 47 | // We will remove the previous rate, so we will need to update the video count attribute |
47 | if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement-- | 48 | if (previousRate.type === 'like') likesToIncrement-- |
48 | else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- | 49 | else if (previousRate.type === 'dislike') dislikesToIncrement-- |
49 | 50 | ||
50 | if (rateType === 'none') { // Destroy previous rate | 51 | if (rateType === 'none') { // Destroy previous rate |
51 | await previousRate.destroy(sequelizeOptions) | 52 | await previousRate.destroy(sequelizeOptions) |
52 | } else { // Update previous rate | 53 | } else { // Update previous rate |
53 | previousRate.type = rateType | 54 | previousRate.type = rateType |
55 | previousRate.url = getRateUrl(rateType, userAccount.Actor, videoInstance) | ||
54 | await previousRate.save(sequelizeOptions) | 56 | await previousRate.save(sequelizeOptions) |
55 | } | 57 | } |
56 | } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate | 58 | } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate |
57 | const query = { | 59 | const query = { |
58 | accountId: accountInstance.id, | 60 | accountId: accountInstance.id, |
59 | videoId: videoInstance.id, | 61 | videoId: videoInstance.id, |
60 | type: rateType | 62 | type: rateType, |
63 | url: getRateUrl(rateType, userAccount.Actor, videoInstance) | ||
61 | } | 64 | } |
62 | 65 | ||
63 | await AccountVideoRateModel.create(query, sequelizeOptions) | 66 | await AccountVideoRateModel.create(query, sequelizeOptions) |
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts new file mode 100644 index 000000000..2db86a2d8 --- /dev/null +++ b/server/controllers/bots.ts | |||
@@ -0,0 +1,101 @@ | |||
1 | import * as express from 'express' | ||
2 | import { asyncMiddleware } from '../middlewares' | ||
3 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../initializers' | ||
4 | import * as sitemapModule from 'sitemap' | ||
5 | import { logger } from '../helpers/logger' | ||
6 | import { VideoModel } from '../models/video/video' | ||
7 | import { VideoChannelModel } from '../models/video/video-channel' | ||
8 | import { AccountModel } from '../models/account/account' | ||
9 | import { cacheRoute } from '../middlewares/cache' | ||
10 | import { buildNSFWFilter } from '../helpers/express-utils' | ||
11 | import { truncate } from 'lodash' | ||
12 | |||
13 | const botsRouter = express.Router() | ||
14 | |||
15 | // Special route that add OpenGraph and oEmbed tags | ||
16 | // Do not use a template engine for a so little thing | ||
17 | botsRouter.use('/sitemap.xml', | ||
18 | asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP)), | ||
19 | asyncMiddleware(getSitemap) | ||
20 | ) | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | export { | ||
25 | botsRouter | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | async function getSitemap (req: express.Request, res: express.Response) { | ||
31 | let urls = getSitemapBasicUrls() | ||
32 | |||
33 | urls = urls.concat(await getSitemapLocalVideoUrls()) | ||
34 | urls = urls.concat(await getSitemapVideoChannelUrls()) | ||
35 | urls = urls.concat(await getSitemapAccountUrls()) | ||
36 | |||
37 | const sitemap = sitemapModule.createSitemap({ | ||
38 | hostname: CONFIG.WEBSERVER.URL, | ||
39 | urls: urls | ||
40 | }) | ||
41 | |||
42 | sitemap.toXML((err, xml) => { | ||
43 | if (err) { | ||
44 | logger.error('Cannot generate sitemap.', { err }) | ||
45 | return res.sendStatus(500) | ||
46 | } | ||
47 | |||
48 | res.header('Content-Type', 'application/xml') | ||
49 | res.send(xml) | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | async function getSitemapVideoChannelUrls () { | ||
54 | const rows = await VideoChannelModel.listLocalsForSitemap('createdAt') | ||
55 | |||
56 | return rows.map(channel => ({ | ||
57 | url: CONFIG.WEBSERVER.URL + '/video-channels/' + channel.Actor.preferredUsername | ||
58 | })) | ||
59 | } | ||
60 | |||
61 | async function getSitemapAccountUrls () { | ||
62 | const rows = await AccountModel.listLocalsForSitemap('createdAt') | ||
63 | |||
64 | return rows.map(channel => ({ | ||
65 | url: CONFIG.WEBSERVER.URL + '/accounts/' + channel.Actor.preferredUsername | ||
66 | })) | ||
67 | } | ||
68 | |||
69 | async function getSitemapLocalVideoUrls () { | ||
70 | const resultList = await VideoModel.listForApi({ | ||
71 | start: 0, | ||
72 | count: undefined, | ||
73 | sort: 'createdAt', | ||
74 | includeLocalVideos: true, | ||
75 | nsfw: buildNSFWFilter(), | ||
76 | filter: 'local', | ||
77 | withFiles: false | ||
78 | }) | ||
79 | |||
80 | return resultList.data.map(v => ({ | ||
81 | url: CONFIG.WEBSERVER.URL + '/videos/watch/' + v.uuid, | ||
82 | video: [ | ||
83 | { | ||
84 | title: v.name, | ||
85 | // Sitemap description should be < 2000 characters | ||
86 | description: truncate(v.description || v.name, { length: 2000, omission: '...' }), | ||
87 | player_loc: CONFIG.WEBSERVER.URL + '/videos/embed/' + v.uuid, | ||
88 | thumbnail_loc: CONFIG.WEBSERVER.URL + v.getThumbnailStaticPath() | ||
89 | } | ||
90 | ] | ||
91 | })) | ||
92 | } | ||
93 | |||
94 | function getSitemapBasicUrls () { | ||
95 | const paths = [ | ||
96 | '/about/instance', | ||
97 | '/videos/local' | ||
98 | ] | ||
99 | |||
100 | return paths.map(p => ({ url: CONFIG.WEBSERVER.URL + p })) | ||
101 | } | ||
diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 73b40cf65..f17f2a5d2 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { root } from '../helpers/core-utils' | 3 | import { root } from '../helpers/core-utils' |
4 | import { ACCEPT_HEADERS, STATIC_MAX_AGE } from '../initializers' | 4 | import { ACCEPT_HEADERS, STATIC_MAX_AGE } from '../initializers' |
5 | import { asyncMiddleware } from '../middlewares' | 5 | import { asyncMiddleware, embedCSP } from '../middlewares' |
6 | import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '../../shared/models/i18n/i18n' | 6 | import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '../../shared/models/i18n/i18n' |
7 | import { ClientHtml } from '../lib/client-html' | 7 | import { ClientHtml } from '../lib/client-html' |
8 | import { logger } from '../helpers/logger' | 8 | import { logger } from '../helpers/logger' |
@@ -16,21 +16,20 @@ const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html') | |||
16 | 16 | ||
17 | // Special route that add OpenGraph and oEmbed tags | 17 | // Special route that add OpenGraph and oEmbed tags |
18 | // Do not use a template engine for a so little thing | 18 | // Do not use a template engine for a so little thing |
19 | clientsRouter.use('/videos/watch/:id', | 19 | clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage)) |
20 | asyncMiddleware(generateWatchHtmlPage) | ||
21 | ) | ||
22 | 20 | ||
23 | clientsRouter.use('' + | 21 | clientsRouter.use( |
24 | '/videos/embed', | 22 | '/videos/embed', |
25 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 23 | embedCSP, |
24 | (req: express.Request, res: express.Response) => { | ||
26 | res.removeHeader('X-Frame-Options') | 25 | res.removeHeader('X-Frame-Options') |
27 | res.sendFile(embedPath) | 26 | res.sendFile(embedPath) |
28 | } | 27 | } |
29 | ) | 28 | ) |
30 | clientsRouter.use('' + | 29 | clientsRouter.use( |
31 | '/videos/test-embed', (req: express.Request, res: express.Response, next: express.NextFunction) => { | 30 | '/videos/test-embed', |
32 | res.sendFile(testEmbedPath) | 31 | (req: express.Request, res: express.Response) => res.sendFile(testEmbedPath) |
33 | }) | 32 | ) |
34 | 33 | ||
35 | // Static HTML/CSS/JS client files | 34 | // Static HTML/CSS/JS client files |
36 | 35 | ||
@@ -89,7 +88,7 @@ export { | |||
89 | // --------------------------------------------------------------------------- | 88 | // --------------------------------------------------------------------------- |
90 | 89 | ||
91 | async function generateHTMLPage (req: express.Request, res: express.Response, paramLang?: string) { | 90 | async function generateHTMLPage (req: express.Request, res: express.Response, paramLang?: string) { |
92 | const html = await ClientHtml.getIndexHTML(req, res, paramLang) | 91 | const html = await ClientHtml.getDefaultHTMLPage(req, res, paramLang) |
93 | 92 | ||
94 | return sendHTML(html, res) | 93 | return sendHTML(html, res) |
95 | } | 94 | } |
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index b30ad8e8d..960085af1 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -1,7 +1,14 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' | 2 | import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' |
3 | import { THUMBNAILS_SIZE } from '../initializers' | 3 | import { THUMBNAILS_SIZE } from '../initializers' |
4 | import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeedsValidator, videosSortValidator } from '../middlewares' | 4 | import { |
5 | asyncMiddleware, | ||
6 | commonVideosFiltersValidator, | ||
7 | setDefaultSort, | ||
8 | videoCommentsFeedsValidator, | ||
9 | videoFeedsValidator, | ||
10 | videosSortValidator | ||
11 | } from '../middlewares' | ||
5 | import { VideoModel } from '../models/video/video' | 12 | import { VideoModel } from '../models/video/video' |
6 | import * as Feed from 'pfeed' | 13 | import * as Feed from 'pfeed' |
7 | import { AccountModel } from '../models/account/account' | 14 | import { AccountModel } from '../models/account/account' |
@@ -22,6 +29,7 @@ feedsRouter.get('/feeds/videos.:format', | |||
22 | videosSortValidator, | 29 | videosSortValidator, |
23 | setDefaultSort, | 30 | setDefaultSort, |
24 | asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), | 31 | asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), |
32 | commonVideosFiltersValidator, | ||
25 | asyncMiddleware(videoFeedsValidator), | 33 | asyncMiddleware(videoFeedsValidator), |
26 | asyncMiddleware(generateVideoFeed) | 34 | asyncMiddleware(generateVideoFeed) |
27 | ) | 35 | ) |
@@ -48,7 +56,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res | |||
48 | 56 | ||
49 | // Adding video items to the feed, one at a time | 57 | // Adding video items to the feed, one at a time |
50 | comments.forEach(comment => { | 58 | comments.forEach(comment => { |
51 | const link = CONFIG.WEBSERVER.URL + '/videos/watch/' + comment.Video.uuid + ';threadId=' + comment.getThreadId() | 59 | const link = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() |
52 | 60 | ||
53 | feed.addItem({ | 61 | feed.addItem({ |
54 | title: `${comment.Video.name} - ${comment.Account.getDisplayName()}`, | 62 | title: `${comment.Video.name} - ${comment.Account.getDisplayName()}`, |
diff --git a/server/controllers/index.ts b/server/controllers/index.ts index 197fa897a..a88a03c79 100644 --- a/server/controllers/index.ts +++ b/server/controllers/index.ts | |||
@@ -6,3 +6,4 @@ export * from './services' | |||
6 | export * from './static' | 6 | export * from './static' |
7 | export * from './webfinger' | 7 | export * from './webfinger' |
8 | export * from './tracker' | 8 | export * from './tracker' |
9 | export * from './bots' | ||
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 75e30353c..b21f9da00 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as cors from 'cors' | 1 | import * as cors from 'cors' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { CONFIG, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' | 3 | import { CONFIG, HLS_PLAYLIST_DIRECTORY, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' |
4 | import { VideosPreviewCache } from '../lib/cache' | 4 | import { VideosPreviewCache } from '../lib/cache' |
5 | import { cacheRoute } from '../middlewares/cache' | 5 | import { cacheRoute } from '../middlewares/cache' |
6 | import { asyncMiddleware, videosGetValidator } from '../middlewares' | 6 | import { asyncMiddleware, videosGetValidator } from '../middlewares' |
@@ -34,18 +34,30 @@ staticRouter.use( | |||
34 | ) | 34 | ) |
35 | 35 | ||
36 | // Videos path for webseeding | 36 | // Videos path for webseeding |
37 | const videosPhysicalPath = CONFIG.STORAGE.VIDEOS_DIR | ||
38 | staticRouter.use( | 37 | staticRouter.use( |
39 | STATIC_PATHS.WEBSEED, | 38 | STATIC_PATHS.WEBSEED, |
40 | cors(), | 39 | cors(), |
41 | express.static(videosPhysicalPath) | 40 | express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video |
42 | ) | 41 | ) |
43 | staticRouter.use( | 42 | staticRouter.use( |
43 | STATIC_PATHS.REDUNDANCY, | ||
44 | cors(), | ||
45 | express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video | ||
46 | ) | ||
47 | |||
48 | staticRouter.use( | ||
44 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', | 49 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', |
45 | asyncMiddleware(videosGetValidator), | 50 | asyncMiddleware(videosGetValidator), |
46 | asyncMiddleware(downloadVideoFile) | 51 | asyncMiddleware(downloadVideoFile) |
47 | ) | 52 | ) |
48 | 53 | ||
54 | // HLS | ||
55 | staticRouter.use( | ||
56 | STATIC_PATHS.PLAYLISTS.HLS, | ||
57 | cors(), | ||
58 | express.static(HLS_PLAYLIST_DIRECTORY, { fallthrough: false }) // 404 if the file does not exist | ||
59 | ) | ||
60 | |||
49 | // Thumbnails path for express | 61 | // Thumbnails path for express |
50 | const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR | 62 | const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR |
51 | staticRouter.use( | 63 | staticRouter.use( |
@@ -131,6 +143,12 @@ staticRouter.use('/.well-known/dnt/', | |||
131 | } | 143 | } |
132 | ) | 144 | ) |
133 | 145 | ||
146 | staticRouter.use('/.well-known/change-password', | ||
147 | (_, res: express.Response) => { | ||
148 | res.redirect('/my-account/settings') | ||
149 | } | ||
150 | ) | ||
151 | |||
134 | // --------------------------------------------------------------------------- | 152 | // --------------------------------------------------------------------------- |
135 | 153 | ||
136 | export { | 154 | export { |
diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts index 9bc7586d1..8b77d9de7 100644 --- a/server/controllers/tracker.ts +++ b/server/controllers/tracker.ts | |||
@@ -6,6 +6,8 @@ import * as proxyAddr from 'proxy-addr' | |||
6 | import { Server as WebSocketServer } from 'ws' | 6 | import { Server as WebSocketServer } from 'ws' |
7 | import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants' | 7 | import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants' |
8 | import { VideoFileModel } from '../models/video/video-file' | 8 | import { VideoFileModel } from '../models/video/video-file' |
9 | import { parse } from 'url' | ||
10 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
9 | 11 | ||
10 | const TrackerServer = bitTorrentTracker.Server | 12 | const TrackerServer = bitTorrentTracker.Server |
11 | 13 | ||
@@ -20,7 +22,7 @@ const trackerServer = new TrackerServer({ | |||
20 | udp: false, | 22 | udp: false, |
21 | ws: false, | 23 | ws: false, |
22 | dht: false, | 24 | dht: false, |
23 | filter: function (infoHash, params, cb) { | 25 | filter: async function (infoHash, params, cb) { |
24 | let ip: string | 26 | let ip: string |
25 | 27 | ||
26 | if (params.type === 'ws') { | 28 | if (params.type === 'ws') { |
@@ -31,19 +33,25 @@ const trackerServer = new TrackerServer({ | |||
31 | 33 | ||
32 | const key = ip + '-' + infoHash | 34 | const key = ip + '-' + infoHash |
33 | 35 | ||
34 | peersIps[ip] = peersIps[ip] ? peersIps[ip] + 1 : 1 | 36 | peersIps[ ip ] = peersIps[ ip ] ? peersIps[ ip ] + 1 : 1 |
35 | peersIpInfoHash[key] = peersIpInfoHash[key] ? peersIpInfoHash[key] + 1 : 1 | 37 | peersIpInfoHash[ key ] = peersIpInfoHash[ key ] ? peersIpInfoHash[ key ] + 1 : 1 |
36 | 38 | ||
37 | if (peersIpInfoHash[key] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) { | 39 | if (peersIpInfoHash[ key ] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) { |
38 | return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`)) | 40 | return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`)) |
39 | } | 41 | } |
40 | 42 | ||
41 | VideoFileModel.isInfohashExists(infoHash) | 43 | try { |
42 | .then(exists => { | 44 | const videoFileExists = await VideoFileModel.doesInfohashExist(infoHash) |
43 | if (exists === false) return cb(new Error(`Unknown infoHash ${infoHash}`)) | 45 | if (videoFileExists === true) return cb() |
44 | 46 | ||
45 | return cb() | 47 | const playlistExists = await VideoStreamingPlaylistModel.doesInfohashExist(infoHash) |
46 | }) | 48 | if (playlistExists === true) return cb() |
49 | |||
50 | return cb(new Error(`Unknown infoHash ${infoHash}`)) | ||
51 | } catch (err) { | ||
52 | logger.error('Error in tracker filter.', { err }) | ||
53 | return cb(err) | ||
54 | } | ||
47 | } | 55 | } |
48 | }) | 56 | }) |
49 | 57 | ||
@@ -59,16 +67,26 @@ const onHttpRequest = trackerServer.onHttpRequest.bind(trackerServer) | |||
59 | trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' })) | 67 | trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' })) |
60 | trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' })) | 68 | trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' })) |
61 | 69 | ||
62 | function createWebsocketServer (app: express.Application) { | 70 | function createWebsocketTrackerServer (app: express.Application) { |
63 | const server = http.createServer(app) | 71 | const server = http.createServer(app) |
64 | const wss = new WebSocketServer({ server: server, path: '/tracker/socket' }) | 72 | const wss = new WebSocketServer({ noServer: true }) |
73 | |||
65 | wss.on('connection', function (ws, req) { | 74 | wss.on('connection', function (ws, req) { |
66 | const ip = proxyAddr(req, CONFIG.TRUST_PROXY) | 75 | ws['ip'] = proxyAddr(req, CONFIG.TRUST_PROXY) |
67 | ws['ip'] = ip | ||
68 | 76 | ||
69 | trackerServer.onWebSocketConnection(ws) | 77 | trackerServer.onWebSocketConnection(ws) |
70 | }) | 78 | }) |
71 | 79 | ||
80 | server.on('upgrade', (request, socket, head) => { | ||
81 | const pathname = parse(request.url).pathname | ||
82 | |||
83 | if (pathname === '/tracker/socket') { | ||
84 | wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request)) | ||
85 | } | ||
86 | |||
87 | // Don't destroy socket, we have Socket.IO too | ||
88 | }) | ||
89 | |||
72 | return server | 90 | return server |
73 | } | 91 | } |
74 | 92 | ||
@@ -76,7 +94,7 @@ function createWebsocketServer (app: express.Application) { | |||
76 | 94 | ||
77 | export { | 95 | export { |
78 | trackerRouter, | 96 | trackerRouter, |
79 | createWebsocketServer | 97 | createWebsocketTrackerServer |
80 | } | 98 | } |
81 | 99 | ||
82 | // --------------------------------------------------------------------------- | 100 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index 2469b37b1..62d78373e 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as validator from 'validator' | 2 | import * as validator from 'validator' |
3 | import { ResultList } from '../../shared/models' | 3 | import { ResultList } from '../../shared/models' |
4 | import { Activity, ActivityPubActor } from '../../shared/models/activitypub' | 4 | import { Activity } from '../../shared/models/activitypub' |
5 | import { ACTIVITY_PUB } from '../initializers' | 5 | import { ACTIVITY_PUB } from '../initializers' |
6 | import { ActorModel } from '../models/activitypub/actor' | 6 | import { ActorModel } from '../models/activitypub/actor' |
7 | import { signObject } from './peertube-crypto' | 7 | import { signJsonLDObject } from './peertube-crypto' |
8 | import { pageToStartAndCount } from './core-utils' | 8 | import { pageToStartAndCount } from './core-utils' |
9 | import { parse } from 'url' | ||
9 | 10 | ||
10 | function activityPubContextify <T> (data: T) { | 11 | function activityPubContextify <T> (data: T) { |
11 | return Object.assign(data, { | 12 | return Object.assign(data, { |
@@ -14,25 +15,26 @@ function activityPubContextify <T> (data: T) { | |||
14 | 'https://w3id.org/security/v1', | 15 | 'https://w3id.org/security/v1', |
15 | { | 16 | { |
16 | RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', | 17 | RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', |
17 | pt: 'https://joinpeertube.org/ns', | 18 | pt: 'https://joinpeertube.org/ns#', |
18 | schema: 'http://schema.org#', | 19 | sc: 'http://schema.org#', |
19 | Hashtag: 'as:Hashtag', | 20 | Hashtag: 'as:Hashtag', |
20 | uuid: 'schema:identifier', | 21 | uuid: 'sc:identifier', |
21 | category: 'schema:category', | 22 | category: 'sc:category', |
22 | licence: 'schema:license', | 23 | licence: 'sc:license', |
23 | subtitleLanguage: 'schema:subtitleLanguage', | 24 | subtitleLanguage: 'sc:subtitleLanguage', |
24 | sensitive: 'as:sensitive', | 25 | sensitive: 'as:sensitive', |
25 | language: 'schema:inLanguage', | 26 | language: 'sc:inLanguage', |
26 | views: 'schema:Number', | 27 | views: 'sc:Number', |
27 | stats: 'schema:Number', | 28 | state: 'sc:Number', |
28 | size: 'schema:Number', | 29 | size: 'sc:Number', |
29 | fps: 'schema:Number', | 30 | fps: 'sc:Number', |
30 | commentsEnabled: 'schema:Boolean', | 31 | commentsEnabled: 'sc:Boolean', |
31 | downloadEnabled: 'schema:Boolean', | 32 | downloadEnabled: 'sc:Boolean', |
32 | waitTranscoding: 'schema:Boolean', | 33 | waitTranscoding: 'sc:Boolean', |
33 | expires: 'schema:expires', | 34 | expires: 'sc:expires', |
34 | support: 'schema:Text', | 35 | support: 'sc:Text', |
35 | CacheFile: 'pt:CacheFile' | 36 | CacheFile: 'pt:CacheFile', |
37 | Infohash: 'pt:Infohash' | ||
36 | }, | 38 | }, |
37 | { | 39 | { |
38 | likes: { | 40 | likes: { |
@@ -57,16 +59,16 @@ function activityPubContextify <T> (data: T) { | |||
57 | } | 59 | } |
58 | 60 | ||
59 | type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> | 61 | type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> |
60 | async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) { | 62 | async function activityPubCollectionPagination (baseUrl: string, handler: ActivityPubCollectionPaginationHandler, page?: any) { |
61 | if (!page || !validator.isInt(page)) { | 63 | if (!page || !validator.isInt(page)) { |
62 | // We just display the first page URL, we only need the total items | 64 | // We just display the first page URL, we only need the total items |
63 | const result = await handler(0, 1) | 65 | const result = await handler(0, 1) |
64 | 66 | ||
65 | return { | 67 | return { |
66 | id: url, | 68 | id: baseUrl, |
67 | type: 'OrderedCollection', | 69 | type: 'OrderedCollection', |
68 | totalItems: result.total, | 70 | totalItems: result.total, |
69 | first: url + '?page=1' | 71 | first: baseUrl + '?page=1' |
70 | } | 72 | } |
71 | } | 73 | } |
72 | 74 | ||
@@ -81,19 +83,19 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu | |||
81 | 83 | ||
82 | // There are more results | 84 | // There are more results |
83 | if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) { | 85 | if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) { |
84 | next = url + '?page=' + (page + 1) | 86 | next = baseUrl + '?page=' + (page + 1) |
85 | } | 87 | } |
86 | 88 | ||
87 | if (page > 1) { | 89 | if (page > 1) { |
88 | prev = url + '?page=' + (page - 1) | 90 | prev = baseUrl + '?page=' + (page - 1) |
89 | } | 91 | } |
90 | 92 | ||
91 | return { | 93 | return { |
92 | id: url + '?page=' + page, | 94 | id: baseUrl + '?page=' + page, |
93 | type: 'OrderedCollectionPage', | 95 | type: 'OrderedCollectionPage', |
94 | prev, | 96 | prev, |
95 | next, | 97 | next, |
96 | partOf: url, | 98 | partOf: baseUrl, |
97 | orderedItems: result.data, | 99 | orderedItems: result.data, |
98 | totalItems: result.total | 100 | totalItems: result.total |
99 | } | 101 | } |
@@ -103,19 +105,27 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu | |||
103 | function buildSignedActivity (byActor: ActorModel, data: Object) { | 105 | function buildSignedActivity (byActor: ActorModel, data: Object) { |
104 | const activity = activityPubContextify(data) | 106 | const activity = activityPubContextify(data) |
105 | 107 | ||
106 | return signObject(byActor, activity) as Promise<Activity> | 108 | return signJsonLDObject(byActor, activity) as Promise<Activity> |
107 | } | 109 | } |
108 | 110 | ||
109 | function getActorUrl (activityActor: string | ActivityPubActor) { | 111 | function getAPId (activity: string | { id: string }) { |
110 | if (typeof activityActor === 'string') return activityActor | 112 | if (typeof activity === 'string') return activity |
111 | 113 | ||
112 | return activityActor.id | 114 | return activity.id |
115 | } | ||
116 | |||
117 | function checkUrlsSameHost (url1: string, url2: string) { | ||
118 | const idHost = parse(url1).host | ||
119 | const actorHost = parse(url2).host | ||
120 | |||
121 | return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase() | ||
113 | } | 122 | } |
114 | 123 | ||
115 | // --------------------------------------------------------------------------- | 124 | // --------------------------------------------------------------------------- |
116 | 125 | ||
117 | export { | 126 | export { |
118 | getActorUrl, | 127 | checkUrlsSameHost, |
128 | getAPId, | ||
119 | activityPubContextify, | 129 | activityPubContextify, |
120 | activityPubCollectionPagination, | 130 | activityPubCollectionPagination, |
121 | buildSignedActivity | 131 | buildSignedActivity |
diff --git a/server/helpers/captions-utils.ts b/server/helpers/captions-utils.ts index 660dce65c..0fb11a125 100644 --- a/server/helpers/captions-utils.ts +++ b/server/helpers/captions-utils.ts | |||
@@ -2,7 +2,7 @@ import { join } from 'path' | |||
2 | import { CONFIG } from '../initializers' | 2 | import { CONFIG } from '../initializers' |
3 | import { VideoCaptionModel } from '../models/video/video-caption' | 3 | import { VideoCaptionModel } from '../models/video/video-caption' |
4 | import * as srt2vtt from 'srt-to-vtt' | 4 | import * as srt2vtt from 'srt-to-vtt' |
5 | import { createReadStream, createWriteStream, remove, rename } from 'fs-extra' | 5 | import { createReadStream, createWriteStream, remove, move } from 'fs-extra' |
6 | 6 | ||
7 | async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) { | 7 | async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) { |
8 | const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR | 8 | const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR |
@@ -13,7 +13,7 @@ async function moveAndProcessCaptionFile (physicalFile: { filename: string, path | |||
13 | await convertSrtToVtt(physicalFile.path, destination) | 13 | await convertSrtToVtt(physicalFile.path, destination) |
14 | await remove(physicalFile.path) | 14 | await remove(physicalFile.path) |
15 | } else { // Just move the vtt file | 15 | } else { // Just move the vtt file |
16 | await rename(physicalFile.path, destination) | 16 | await move(physicalFile.path, destination, { overwrite: true }) |
17 | } | 17 | } |
18 | 18 | ||
19 | // This is important in case if there is another attempt in the retry process | 19 | // This is important in case if there is another attempt in the retry process |
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 00bc0bdda..f38b82d97 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -5,12 +5,31 @@ | |||
5 | 5 | ||
6 | import * as bcrypt from 'bcrypt' | 6 | import * as bcrypt from 'bcrypt' |
7 | import * as createTorrent from 'create-torrent' | 7 | import * as createTorrent from 'create-torrent' |
8 | import { createHash, pseudoRandomBytes } from 'crypto' | 8 | import { createHash, HexBase64Latin1Encoding, pseudoRandomBytes } from 'crypto' |
9 | import { isAbsolute, join } from 'path' | 9 | import { isAbsolute, join } from 'path' |
10 | import * as pem from 'pem' | 10 | import * as pem from 'pem' |
11 | import { URL } from 'url' | 11 | import { URL } from 'url' |
12 | import { truncate } from 'lodash' | 12 | import { truncate } from 'lodash' |
13 | import { exec } from 'child_process' | 13 | import { exec } from 'child_process' |
14 | import { isArray } from './custom-validators/misc' | ||
15 | |||
16 | const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { | ||
17 | if (!oldObject || typeof oldObject !== 'object') { | ||
18 | return valueConverter(oldObject) | ||
19 | } | ||
20 | |||
21 | if (isArray(oldObject)) { | ||
22 | return oldObject.map(e => objectConverter(e, keyConverter, valueConverter)) | ||
23 | } | ||
24 | |||
25 | const newObject = {} | ||
26 | Object.keys(oldObject).forEach(oldKey => { | ||
27 | const newKey = keyConverter(oldKey) | ||
28 | newObject[ newKey ] = objectConverter(oldObject[ oldKey ], keyConverter, valueConverter) | ||
29 | }) | ||
30 | |||
31 | return newObject | ||
32 | } | ||
14 | 33 | ||
15 | const timeTable = { | 34 | const timeTable = { |
16 | ms: 1, | 35 | ms: 1, |
@@ -21,6 +40,7 @@ const timeTable = { | |||
21 | week: 3600000 * 24 * 7, | 40 | week: 3600000 * 24 * 7, |
22 | month: 3600000 * 24 * 30 | 41 | month: 3600000 * 24 * 30 |
23 | } | 42 | } |
43 | |||
24 | export function parseDuration (duration: number | string): number { | 44 | export function parseDuration (duration: number | string): number { |
25 | if (typeof duration === 'number') return duration | 45 | if (typeof duration === 'number') return duration |
26 | 46 | ||
@@ -41,6 +61,53 @@ export function parseDuration (duration: number | string): number { | |||
41 | throw new Error('Duration could not be properly parsed') | 61 | throw new Error('Duration could not be properly parsed') |
42 | } | 62 | } |
43 | 63 | ||
64 | export function parseBytes (value: string | number): number { | ||
65 | if (typeof value === 'number') return value | ||
66 | |||
67 | const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/ | ||
68 | const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/ | ||
69 | const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/ | ||
70 | const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/ | ||
71 | const t = /^(\d+)\s*TB$/ | ||
72 | const g = /^(\d+)\s*GB$/ | ||
73 | const m = /^(\d+)\s*MB$/ | ||
74 | const b = /^(\d+)\s*B$/ | ||
75 | let match | ||
76 | |||
77 | if (value.match(tgm)) { | ||
78 | match = value.match(tgm) | ||
79 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
80 | + parseInt(match[2], 10) * 1024 * 1024 * 1024 | ||
81 | + parseInt(match[3], 10) * 1024 * 1024 | ||
82 | } else if (value.match(tg)) { | ||
83 | match = value.match(tg) | ||
84 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
85 | + parseInt(match[2], 10) * 1024 * 1024 * 1024 | ||
86 | } else if (value.match(tm)) { | ||
87 | match = value.match(tm) | ||
88 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
89 | + parseInt(match[2], 10) * 1024 * 1024 | ||
90 | } else if (value.match(gm)) { | ||
91 | match = value.match(gm) | ||
92 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 | ||
93 | + parseInt(match[2], 10) * 1024 * 1024 | ||
94 | } else if (value.match(t)) { | ||
95 | match = value.match(t) | ||
96 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
97 | } else if (value.match(g)) { | ||
98 | match = value.match(g) | ||
99 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 | ||
100 | } else if (value.match(m)) { | ||
101 | match = value.match(m) | ||
102 | return parseInt(match[1], 10) * 1024 * 1024 | ||
103 | } else if (value.match(b)) { | ||
104 | match = value.match(b) | ||
105 | return parseInt(match[1], 10) * 1024 | ||
106 | } else { | ||
107 | return parseInt(value, 10) | ||
108 | } | ||
109 | } | ||
110 | |||
44 | function sanitizeUrl (url: string) { | 111 | function sanitizeUrl (url: string) { |
45 | const urlObject = new URL(url) | 112 | const urlObject = new URL(url) |
46 | 113 | ||
@@ -126,8 +193,12 @@ function peertubeTruncate (str: string, maxLength: number) { | |||
126 | return truncate(str, options) | 193 | return truncate(str, options) |
127 | } | 194 | } |
128 | 195 | ||
129 | function sha256 (str: string) { | 196 | function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') { |
130 | return createHash('sha256').update(str).digest('hex') | 197 | return createHash('sha256').update(str).digest(encoding) |
198 | } | ||
199 | |||
200 | function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') { | ||
201 | return createHash('sha1').update(str).digest(encoding) | ||
131 | } | 202 | } |
132 | 203 | ||
133 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { | 204 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { |
@@ -187,6 +258,7 @@ export { | |||
187 | isTestInstance, | 258 | isTestInstance, |
188 | isProdInstance, | 259 | isProdInstance, |
189 | 260 | ||
261 | objectConverter, | ||
190 | root, | 262 | root, |
191 | escapeHTML, | 263 | escapeHTML, |
192 | pageToStartAndCount, | 264 | pageToStartAndCount, |
@@ -194,7 +266,9 @@ export { | |||
194 | sanitizeHost, | 266 | sanitizeHost, |
195 | buildPath, | 267 | buildPath, |
196 | peertubeTruncate, | 268 | peertubeTruncate, |
269 | |||
197 | sha256, | 270 | sha256, |
271 | sha1, | ||
198 | 272 | ||
199 | promisify0, | 273 | promisify0, |
200 | promisify1, | 274 | promisify1, |
diff --git a/server/helpers/custom-jsonld-signature.ts b/server/helpers/custom-jsonld-signature.ts index e4f28018e..27a187db1 100644 --- a/server/helpers/custom-jsonld-signature.ts +++ b/server/helpers/custom-jsonld-signature.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as AsyncLRU from 'async-lru' | 1 | import * as AsyncLRU from 'async-lru' |
2 | import * as jsonld from 'jsonld/' | 2 | import * as jsonld from 'jsonld' |
3 | import * as jsig from 'jsonld-signatures' | 3 | import * as jsig from 'jsonld-signatures' |
4 | 4 | ||
5 | const nodeDocumentLoader = jsonld.documentLoaders.node() | 5 | const nodeDocumentLoader = jsonld.documentLoaders.node() |
@@ -17,4 +17,4 @@ jsonld.documentLoader = (url, cb) => { | |||
17 | 17 | ||
18 | jsig.use('jsonld', jsonld) | 18 | jsig.use('jsonld', jsonld) |
19 | 19 | ||
20 | export { jsig } | 20 | export { jsig, jsonld } |
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index 2562ead9b..b24590d9d 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts | |||
@@ -1,26 +1,14 @@ | |||
1 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
3 | import { | 3 | import { sanitizeAndCheckActorObject } from './actor' |
4 | isActorAcceptActivityValid, | 4 | import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' |
5 | isActorDeleteActivityValid, | 5 | import { isDislikeActivityValid } from './rate' |
6 | isActorFollowActivityValid, | 6 | import { sanitizeAndCheckVideoCommentObject } from './video-comments' |
7 | isActorRejectActivityValid, | 7 | import { sanitizeAndCheckVideoTorrentObject } from './videos' |
8 | isActorUpdateActivityValid | ||
9 | } from './actor' | ||
10 | import { isAnnounceActivityValid } from './announce' | ||
11 | import { isActivityPubUrlValid } from './misc' | ||
12 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | ||
13 | import { isUndoActivityValid } from './undo' | ||
14 | import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments' | ||
15 | import { | ||
16 | isVideoFlagValid, | ||
17 | isVideoTorrentDeleteActivityValid, | ||
18 | sanitizeAndCheckVideoTorrentCreateActivity, | ||
19 | sanitizeAndCheckVideoTorrentUpdateActivity | ||
20 | } from './videos' | ||
21 | import { isViewActivityValid } from './view' | 8 | import { isViewActivityValid } from './view' |
22 | import { exists } from '../misc' | 9 | import { exists } from '../misc' |
23 | import { isCacheFileCreateActivityValid, isCacheFileUpdateActivityValid } from './cache-file' | 10 | import { isCacheFileObjectValid } from './cache-file' |
11 | import { isFlagActivityValid } from './flag' | ||
24 | 12 | ||
25 | function isRootActivityValid (activity: any) { | 13 | function isRootActivityValid (activity: any) { |
26 | return Array.isArray(activity['@context']) && ( | 14 | return Array.isArray(activity['@context']) && ( |
@@ -46,7 +34,10 @@ const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean | |||
46 | Reject: checkRejectActivity, | 34 | Reject: checkRejectActivity, |
47 | Announce: checkAnnounceActivity, | 35 | Announce: checkAnnounceActivity, |
48 | Undo: checkUndoActivity, | 36 | Undo: checkUndoActivity, |
49 | Like: checkLikeActivity | 37 | Like: checkLikeActivity, |
38 | View: checkViewActivity, | ||
39 | Flag: checkFlagActivity, | ||
40 | Dislike: checkDislikeActivity | ||
50 | } | 41 | } |
51 | 42 | ||
52 | function isActivityValid (activity: any) { | 43 | function isActivityValid (activity: any) { |
@@ -66,47 +57,79 @@ export { | |||
66 | 57 | ||
67 | // --------------------------------------------------------------------------- | 58 | // --------------------------------------------------------------------------- |
68 | 59 | ||
60 | function checkViewActivity (activity: any) { | ||
61 | return isBaseActivityValid(activity, 'View') && | ||
62 | isViewActivityValid(activity) | ||
63 | } | ||
64 | |||
65 | function checkFlagActivity (activity: any) { | ||
66 | return isBaseActivityValid(activity, 'Flag') && | ||
67 | isFlagActivityValid(activity) | ||
68 | } | ||
69 | |||
70 | function checkDislikeActivity (activity: any) { | ||
71 | return isBaseActivityValid(activity, 'Dislike') && | ||
72 | isDislikeActivityValid(activity) | ||
73 | } | ||
74 | |||
69 | function checkCreateActivity (activity: any) { | 75 | function checkCreateActivity (activity: any) { |
70 | return isViewActivityValid(activity) || | 76 | return isBaseActivityValid(activity, 'Create') && |
71 | isDislikeActivityValid(activity) || | 77 | ( |
72 | sanitizeAndCheckVideoTorrentCreateActivity(activity) || | 78 | isViewActivityValid(activity.object) || |
73 | isVideoFlagValid(activity) || | 79 | isDislikeActivityValid(activity.object) || |
74 | isVideoCommentCreateActivityValid(activity) || | 80 | isFlagActivityValid(activity.object) || |
75 | isCacheFileCreateActivityValid(activity) | 81 | |
82 | isCacheFileObjectValid(activity.object) || | ||
83 | sanitizeAndCheckVideoCommentObject(activity.object) || | ||
84 | sanitizeAndCheckVideoTorrentObject(activity.object) | ||
85 | ) | ||
76 | } | 86 | } |
77 | 87 | ||
78 | function checkUpdateActivity (activity: any) { | 88 | function checkUpdateActivity (activity: any) { |
79 | return isCacheFileUpdateActivityValid(activity) || | 89 | return isBaseActivityValid(activity, 'Update') && |
80 | sanitizeAndCheckVideoTorrentUpdateActivity(activity) || | 90 | ( |
81 | isActorUpdateActivityValid(activity) | 91 | isCacheFileObjectValid(activity.object) || |
92 | sanitizeAndCheckVideoTorrentObject(activity.object) || | ||
93 | sanitizeAndCheckActorObject(activity.object) | ||
94 | ) | ||
82 | } | 95 | } |
83 | 96 | ||
84 | function checkDeleteActivity (activity: any) { | 97 | function checkDeleteActivity (activity: any) { |
85 | return isVideoTorrentDeleteActivityValid(activity) || | 98 | // We don't really check objects |
86 | isActorDeleteActivityValid(activity) || | 99 | return isBaseActivityValid(activity, 'Delete') && |
87 | isVideoCommentDeleteActivityValid(activity) | 100 | isObjectValid(activity.object) |
88 | } | 101 | } |
89 | 102 | ||
90 | function checkFollowActivity (activity: any) { | 103 | function checkFollowActivity (activity: any) { |
91 | return isActorFollowActivityValid(activity) | 104 | return isBaseActivityValid(activity, 'Follow') && |
105 | isObjectValid(activity.object) | ||
92 | } | 106 | } |
93 | 107 | ||
94 | function checkAcceptActivity (activity: any) { | 108 | function checkAcceptActivity (activity: any) { |
95 | return isActorAcceptActivityValid(activity) | 109 | return isBaseActivityValid(activity, 'Accept') |
96 | } | 110 | } |
97 | 111 | ||
98 | function checkRejectActivity (activity: any) { | 112 | function checkRejectActivity (activity: any) { |
99 | return isActorRejectActivityValid(activity) | 113 | return isBaseActivityValid(activity, 'Reject') |
100 | } | 114 | } |
101 | 115 | ||
102 | function checkAnnounceActivity (activity: any) { | 116 | function checkAnnounceActivity (activity: any) { |
103 | return isAnnounceActivityValid(activity) | 117 | return isBaseActivityValid(activity, 'Announce') && |
118 | isObjectValid(activity.object) | ||
104 | } | 119 | } |
105 | 120 | ||
106 | function checkUndoActivity (activity: any) { | 121 | function checkUndoActivity (activity: any) { |
107 | return isUndoActivityValid(activity) | 122 | return isBaseActivityValid(activity, 'Undo') && |
123 | ( | ||
124 | checkFollowActivity(activity.object) || | ||
125 | checkLikeActivity(activity.object) || | ||
126 | checkDislikeActivity(activity.object) || | ||
127 | checkAnnounceActivity(activity.object) || | ||
128 | checkCreateActivity(activity.object) | ||
129 | ) | ||
108 | } | 130 | } |
109 | 131 | ||
110 | function checkLikeActivity (activity: any) { | 132 | function checkLikeActivity (activity: any) { |
111 | return isLikeActivityValid(activity) | 133 | return isBaseActivityValid(activity, 'Like') && |
134 | isObjectValid(activity.object) | ||
112 | } | 135 | } |
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts index 77c003cdf..c05f60f14 100644 --- a/server/helpers/custom-validators/activitypub/actor.ts +++ b/server/helpers/custom-validators/activitypub/actor.ts | |||
@@ -27,7 +27,8 @@ function isActorPublicKeyValid (publicKey: string) { | |||
27 | validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY) | 27 | validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY) |
28 | } | 28 | } |
29 | 29 | ||
30 | const actorNameRegExp = new RegExp('^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_\.]+$') | 30 | const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.]' |
31 | const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`) | ||
31 | function isActorPreferredUsernameValid (preferredUsername: string) { | 32 | function isActorPreferredUsernameValid (preferredUsername: string) { |
32 | return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp) | 33 | return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp) |
33 | } | 34 | } |
@@ -72,24 +73,10 @@ function isActorDeleteActivityValid (activity: any) { | |||
72 | return isBaseActivityValid(activity, 'Delete') | 73 | return isBaseActivityValid(activity, 'Delete') |
73 | } | 74 | } |
74 | 75 | ||
75 | function isActorFollowActivityValid (activity: any) { | 76 | function sanitizeAndCheckActorObject (object: any) { |
76 | return isBaseActivityValid(activity, 'Follow') && | 77 | normalizeActor(object) |
77 | isActivityPubUrlValid(activity.object) | ||
78 | } | ||
79 | |||
80 | function isActorAcceptActivityValid (activity: any) { | ||
81 | return isBaseActivityValid(activity, 'Accept') | ||
82 | } | ||
83 | |||
84 | function isActorRejectActivityValid (activity: any) { | ||
85 | return isBaseActivityValid(activity, 'Reject') | ||
86 | } | ||
87 | |||
88 | function isActorUpdateActivityValid (activity: any) { | ||
89 | normalizeActor(activity.object) | ||
90 | 78 | ||
91 | return isBaseActivityValid(activity, 'Update') && | 79 | return isActorObjectValid(object) |
92 | isActorObjectValid(activity.object) | ||
93 | } | 80 | } |
94 | 81 | ||
95 | function normalizeActor (actor: any) { | 82 | function normalizeActor (actor: any) { |
@@ -127,6 +114,7 @@ function areValidActorHandles (handles: string[]) { | |||
127 | 114 | ||
128 | export { | 115 | export { |
129 | normalizeActor, | 116 | normalizeActor, |
117 | actorNameAlphabet, | ||
130 | areValidActorHandles, | 118 | areValidActorHandles, |
131 | isActorEndpointsObjectValid, | 119 | isActorEndpointsObjectValid, |
132 | isActorPublicKeyObjectValid, | 120 | isActorPublicKeyObjectValid, |
@@ -137,10 +125,7 @@ export { | |||
137 | isActorObjectValid, | 125 | isActorObjectValid, |
138 | isActorFollowingCountValid, | 126 | isActorFollowingCountValid, |
139 | isActorFollowersCountValid, | 127 | isActorFollowersCountValid, |
140 | isActorFollowActivityValid, | ||
141 | isActorAcceptActivityValid, | ||
142 | isActorRejectActivityValid, | ||
143 | isActorDeleteActivityValid, | 128 | isActorDeleteActivityValid, |
144 | isActorUpdateActivityValid, | 129 | sanitizeAndCheckActorObject, |
145 | isValidActorHandle | 130 | isValidActorHandle |
146 | } | 131 | } |
diff --git a/server/helpers/custom-validators/activitypub/announce.ts b/server/helpers/custom-validators/activitypub/announce.ts deleted file mode 100644 index 0519c6026..000000000 --- a/server/helpers/custom-validators/activitypub/announce.ts +++ /dev/null | |||
@@ -1,13 +0,0 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | ||
2 | |||
3 | function isAnnounceActivityValid (activity: any) { | ||
4 | return isBaseActivityValid(activity, 'Announce') && | ||
5 | ( | ||
6 | isActivityPubUrlValid(activity.object) || | ||
7 | (activity.object && isActivityPubUrlValid(activity.object.id)) | ||
8 | ) | ||
9 | } | ||
10 | |||
11 | export { | ||
12 | isAnnounceActivityValid | ||
13 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts index bd70934c8..21d5c53ca 100644 --- a/server/helpers/custom-validators/activitypub/cache-file.ts +++ b/server/helpers/custom-validators/activitypub/cache-file.ts | |||
@@ -1,28 +1,26 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 1 | import { isActivityPubUrlValid } from './misc' |
2 | import { isRemoteVideoUrlValid } from './videos' | 2 | import { isRemoteVideoUrlValid } from './videos' |
3 | import { isDateValid, exists } from '../misc' | 3 | import { exists, isDateValid } from '../misc' |
4 | import { CacheFileObject } from '../../../../shared/models/activitypub/objects' | 4 | import { CacheFileObject } from '../../../../shared/models/activitypub/objects' |
5 | 5 | ||
6 | function isCacheFileCreateActivityValid (activity: any) { | ||
7 | return isBaseActivityValid(activity, 'Create') && | ||
8 | isCacheFileObjectValid(activity.object) | ||
9 | } | ||
10 | |||
11 | function isCacheFileUpdateActivityValid (activity: any) { | ||
12 | return isBaseActivityValid(activity, 'Update') && | ||
13 | isCacheFileObjectValid(activity.object) | ||
14 | } | ||
15 | |||
16 | function isCacheFileObjectValid (object: CacheFileObject) { | 6 | function isCacheFileObjectValid (object: CacheFileObject) { |
17 | return exists(object) && | 7 | return exists(object) && |
18 | object.type === 'CacheFile' && | 8 | object.type === 'CacheFile' && |
19 | isDateValid(object.expires) && | 9 | isDateValid(object.expires) && |
20 | isActivityPubUrlValid(object.object) && | 10 | isActivityPubUrlValid(object.object) && |
21 | isRemoteVideoUrlValid(object.url) | 11 | (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url)) |
22 | } | 12 | } |
23 | 13 | ||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
24 | export { | 16 | export { |
25 | isCacheFileUpdateActivityValid, | ||
26 | isCacheFileCreateActivityValid, | ||
27 | isCacheFileObjectValid | 17 | isCacheFileObjectValid |
28 | } | 18 | } |
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | function isPlaylistRedundancyUrlValid (url: any) { | ||
23 | return url.type === 'Link' && | ||
24 | (url.mediaType || url.mimeType) === 'application/x-mpegURL' && | ||
25 | isActivityPubUrlValid(url.href) | ||
26 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts new file mode 100644 index 000000000..6452e297c --- /dev/null +++ b/server/helpers/custom-validators/activitypub/flag.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import { isActivityPubUrlValid } from './misc' | ||
2 | import { isVideoAbuseReasonValid } from '../video-abuses' | ||
3 | |||
4 | function isFlagActivityValid (activity: any) { | ||
5 | return activity.type === 'Flag' && | ||
6 | isVideoAbuseReasonValid(activity.content) && | ||
7 | isActivityPubUrlValid(activity.object) | ||
8 | } | ||
9 | |||
10 | // --------------------------------------------------------------------------- | ||
11 | |||
12 | export { | ||
13 | isFlagActivityValid | ||
14 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts index 4e2c57f04..f1762d11c 100644 --- a/server/helpers/custom-validators/activitypub/misc.ts +++ b/server/helpers/custom-validators/activitypub/misc.ts | |||
@@ -28,15 +28,20 @@ function isBaseActivityValid (activity: any, type: string) { | |||
28 | return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && | 28 | return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && |
29 | activity.type === type && | 29 | activity.type === type && |
30 | isActivityPubUrlValid(activity.id) && | 30 | isActivityPubUrlValid(activity.id) && |
31 | exists(activity.actor) && | 31 | isObjectValid(activity.actor) && |
32 | (isActivityPubUrlValid(activity.actor) || isActivityPubUrlValid(activity.actor.id)) && | 32 | isUrlCollectionValid(activity.to) && |
33 | ( | 33 | isUrlCollectionValid(activity.cc) |
34 | activity.to === undefined || | 34 | } |
35 | (Array.isArray(activity.to) && activity.to.every(t => isActivityPubUrlValid(t))) | 35 | |
36 | ) && | 36 | function isUrlCollectionValid (collection: any) { |
37 | return collection === undefined || | ||
38 | (Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t))) | ||
39 | } | ||
40 | |||
41 | function isObjectValid (object: any) { | ||
42 | return exists(object) && | ||
37 | ( | 43 | ( |
38 | activity.cc === undefined || | 44 | isActivityPubUrlValid(object) || isActivityPubUrlValid(object.id) |
39 | (Array.isArray(activity.cc) && activity.cc.every(t => isActivityPubUrlValid(t))) | ||
40 | ) | 45 | ) |
41 | } | 46 | } |
42 | 47 | ||
@@ -57,5 +62,6 @@ export { | |||
57 | isUrlValid, | 62 | isUrlValid, |
58 | isActivityPubUrlValid, | 63 | isActivityPubUrlValid, |
59 | isBaseActivityValid, | 64 | isBaseActivityValid, |
60 | setValidAttributedTo | 65 | setValidAttributedTo, |
66 | isObjectValid | ||
61 | } | 67 | } |
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts index e70bd94b8..ba68e8074 100644 --- a/server/helpers/custom-validators/activitypub/rate.ts +++ b/server/helpers/custom-validators/activitypub/rate.ts | |||
@@ -1,20 +1,13 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 1 | import { isActivityPubUrlValid, isObjectValid } from './misc' |
2 | |||
3 | function isLikeActivityValid (activity: any) { | ||
4 | return isBaseActivityValid(activity, 'Like') && | ||
5 | isActivityPubUrlValid(activity.object) | ||
6 | } | ||
7 | 2 | ||
8 | function isDislikeActivityValid (activity: any) { | 3 | function isDislikeActivityValid (activity: any) { |
9 | return isBaseActivityValid(activity, 'Create') && | 4 | return activity.type === 'Dislike' && |
10 | activity.object.type === 'Dislike' && | 5 | isActivityPubUrlValid(activity.actor) && |
11 | isActivityPubUrlValid(activity.object.actor) && | 6 | isObjectValid(activity.object) |
12 | isActivityPubUrlValid(activity.object.object) | ||
13 | } | 7 | } |
14 | 8 | ||
15 | // --------------------------------------------------------------------------- | 9 | // --------------------------------------------------------------------------- |
16 | 10 | ||
17 | export { | 11 | export { |
18 | isLikeActivityValid, | ||
19 | isDislikeActivityValid | 12 | isDislikeActivityValid |
20 | } | 13 | } |
diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts deleted file mode 100644 index 578035893..000000000 --- a/server/helpers/custom-validators/activitypub/undo.ts +++ /dev/null | |||
@@ -1,20 +0,0 @@ | |||
1 | import { isActorFollowActivityValid } from './actor' | ||
2 | import { isBaseActivityValid } from './misc' | ||
3 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | ||
4 | import { isAnnounceActivityValid } from './announce' | ||
5 | import { isCacheFileCreateActivityValid } from './cache-file' | ||
6 | |||
7 | function isUndoActivityValid (activity: any) { | ||
8 | return isBaseActivityValid(activity, 'Undo') && | ||
9 | ( | ||
10 | isActorFollowActivityValid(activity.object) || | ||
11 | isLikeActivityValid(activity.object) || | ||
12 | isDislikeActivityValid(activity.object) || | ||
13 | isAnnounceActivityValid(activity.object) || | ||
14 | isCacheFileCreateActivityValid(activity.object) | ||
15 | ) | ||
16 | } | ||
17 | |||
18 | export { | ||
19 | isUndoActivityValid | ||
20 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts index 051c4565a..0415db21c 100644 --- a/server/helpers/custom-validators/activitypub/video-comments.ts +++ b/server/helpers/custom-validators/activitypub/video-comments.ts | |||
@@ -3,11 +3,6 @@ import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' | |||
3 | import { exists, isArray, isDateValid } from '../misc' | 3 | import { exists, isArray, isDateValid } from '../misc' |
4 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 4 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' |
5 | 5 | ||
6 | function isVideoCommentCreateActivityValid (activity: any) { | ||
7 | return isBaseActivityValid(activity, 'Create') && | ||
8 | sanitizeAndCheckVideoCommentObject(activity.object) | ||
9 | } | ||
10 | |||
11 | function sanitizeAndCheckVideoCommentObject (comment: any) { | 6 | function sanitizeAndCheckVideoCommentObject (comment: any) { |
12 | if (!comment || comment.type !== 'Note') return false | 7 | if (!comment || comment.type !== 'Note') return false |
13 | 8 | ||
@@ -25,15 +20,9 @@ function sanitizeAndCheckVideoCommentObject (comment: any) { | |||
25 | ) // Only accept public comments | 20 | ) // Only accept public comments |
26 | } | 21 | } |
27 | 22 | ||
28 | function isVideoCommentDeleteActivityValid (activity: any) { | ||
29 | return isBaseActivityValid(activity, 'Delete') | ||
30 | } | ||
31 | |||
32 | // --------------------------------------------------------------------------- | 23 | // --------------------------------------------------------------------------- |
33 | 24 | ||
34 | export { | 25 | export { |
35 | isVideoCommentCreateActivityValid, | ||
36 | isVideoCommentDeleteActivityValid, | ||
37 | sanitizeAndCheckVideoCommentObject | 26 | sanitizeAndCheckVideoCommentObject |
38 | } | 27 | } |
39 | 28 | ||
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 5015c59dd..53ad0588d 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
2 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' | 2 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' |
3 | import { peertubeTruncate } from '../../core-utils' | 3 | import { peertubeTruncate } from '../../core-utils' |
4 | import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' | 4 | import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' |
5 | import { | 5 | import { |
6 | isVideoDurationValid, | 6 | isVideoDurationValid, |
7 | isVideoNameValid, | 7 | isVideoNameValid, |
@@ -12,29 +12,12 @@ import { | |||
12 | } from '../videos' | 12 | } from '../videos' |
13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
14 | import { VideoState } from '../../../../shared/models/videos' | 14 | import { VideoState } from '../../../../shared/models/videos' |
15 | import { isVideoAbuseReasonValid } from '../video-abuses' | ||
16 | |||
17 | function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { | ||
18 | return isBaseActivityValid(activity, 'Create') && | ||
19 | sanitizeAndCheckVideoTorrentObject(activity.object) | ||
20 | } | ||
21 | 15 | ||
22 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { | 16 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { |
23 | return isBaseActivityValid(activity, 'Update') && | 17 | return isBaseActivityValid(activity, 'Update') && |
24 | sanitizeAndCheckVideoTorrentObject(activity.object) | 18 | sanitizeAndCheckVideoTorrentObject(activity.object) |
25 | } | 19 | } |
26 | 20 | ||
27 | function isVideoTorrentDeleteActivityValid (activity: any) { | ||
28 | return isBaseActivityValid(activity, 'Delete') | ||
29 | } | ||
30 | |||
31 | function isVideoFlagValid (activity: any) { | ||
32 | return isBaseActivityValid(activity, 'Create') && | ||
33 | activity.object.type === 'Flag' && | ||
34 | isVideoAbuseReasonValid(activity.object.content) && | ||
35 | isActivityPubUrlValid(activity.object.object) | ||
36 | } | ||
37 | |||
38 | function isActivityPubVideoDurationValid (value: string) { | 21 | function isActivityPubVideoDurationValid (value: string) { |
39 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | 22 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration |
40 | return exists(value) && | 23 | return exists(value) && |
@@ -83,32 +66,35 @@ function isRemoteVideoUrlValid (url: any) { | |||
83 | 66 | ||
84 | return url.type === 'Link' && | 67 | return url.type === 'Link' && |
85 | ( | 68 | ( |
86 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 && | 69 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) |
70 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType || url.mimeType) !== -1 && | ||
87 | isActivityPubUrlValid(url.href) && | 71 | isActivityPubUrlValid(url.href) && |
88 | validator.isInt(url.height + '', { min: 0 }) && | 72 | validator.isInt(url.height + '', { min: 0 }) && |
89 | validator.isInt(url.size + '', { min: 0 }) && | 73 | validator.isInt(url.size + '', { min: 0 }) && |
90 | (!url.fps || validator.isInt(url.fps + '', { min: -1 })) | 74 | (!url.fps || validator.isInt(url.fps + '', { min: -1 })) |
91 | ) || | 75 | ) || |
92 | ( | 76 | ( |
93 | ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 && | 77 | ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType || url.mimeType) !== -1 && |
94 | isActivityPubUrlValid(url.href) && | 78 | isActivityPubUrlValid(url.href) && |
95 | validator.isInt(url.height + '', { min: 0 }) | 79 | validator.isInt(url.height + '', { min: 0 }) |
96 | ) || | 80 | ) || |
97 | ( | 81 | ( |
98 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 && | 82 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && |
99 | validator.isLength(url.href, { min: 5 }) && | 83 | validator.isLength(url.href, { min: 5 }) && |
100 | validator.isInt(url.height + '', { min: 0 }) | 84 | validator.isInt(url.height + '', { min: 0 }) |
85 | ) || | ||
86 | ( | ||
87 | (url.mediaType || url.mimeType) === 'application/x-mpegURL' && | ||
88 | isActivityPubUrlValid(url.href) && | ||
89 | isArray(url.tag) | ||
101 | ) | 90 | ) |
102 | } | 91 | } |
103 | 92 | ||
104 | // --------------------------------------------------------------------------- | 93 | // --------------------------------------------------------------------------- |
105 | 94 | ||
106 | export { | 95 | export { |
107 | sanitizeAndCheckVideoTorrentCreateActivity, | ||
108 | sanitizeAndCheckVideoTorrentUpdateActivity, | 96 | sanitizeAndCheckVideoTorrentUpdateActivity, |
109 | isVideoTorrentDeleteActivityValid, | ||
110 | isRemoteStringIdentifierValid, | 97 | isRemoteStringIdentifierValid, |
111 | isVideoFlagValid, | ||
112 | sanitizeAndCheckVideoTorrentObject, | 98 | sanitizeAndCheckVideoTorrentObject, |
113 | isRemoteVideoUrlValid | 99 | isRemoteVideoUrlValid |
114 | } | 100 | } |
diff --git a/server/helpers/custom-validators/activitypub/view.ts b/server/helpers/custom-validators/activitypub/view.ts index 7a3aca6f5..41d16469f 100644 --- a/server/helpers/custom-validators/activitypub/view.ts +++ b/server/helpers/custom-validators/activitypub/view.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 1 | import { isActivityPubUrlValid } from './misc' |
2 | 2 | ||
3 | function isViewActivityValid (activity: any) { | 3 | function isViewActivityValid (activity: any) { |
4 | return isBaseActivityValid(activity, 'Create') && | 4 | return activity.type === 'View' && |
5 | activity.object.type === 'View' && | 5 | isActivityPubUrlValid(activity.actor) && |
6 | isActivityPubUrlValid(activity.object.actor) && | 6 | isActivityPubUrlValid(activity.object) |
7 | isActivityPubUrlValid(activity.object.object) | ||
8 | } | 7 | } |
8 | |||
9 | // --------------------------------------------------------------------------- | 9 | // --------------------------------------------------------------------------- |
10 | 10 | ||
11 | export { | 11 | export { |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 6d10a65a8..76647fea2 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -9,6 +9,14 @@ function isArray (value: any) { | |||
9 | return Array.isArray(value) | 9 | return Array.isArray(value) |
10 | } | 10 | } |
11 | 11 | ||
12 | function isNotEmptyIntArray (value: any) { | ||
13 | return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0 | ||
14 | } | ||
15 | |||
16 | function isArrayOf (value: any, validator: (value: any) => boolean) { | ||
17 | return isArray(value) && value.every(v => validator(v)) | ||
18 | } | ||
19 | |||
12 | function isDateValid (value: string) { | 20 | function isDateValid (value: string) { |
13 | return exists(value) && validator.isISO8601(value) | 21 | return exists(value) && validator.isISO8601(value) |
14 | } | 22 | } |
@@ -78,6 +86,8 @@ function isFileValid ( | |||
78 | 86 | ||
79 | export { | 87 | export { |
80 | exists, | 88 | exists, |
89 | isArrayOf, | ||
90 | isNotEmptyIntArray, | ||
81 | isArray, | 91 | isArray, |
82 | isIdValid, | 92 | isIdValid, |
83 | isUUIDValid, | 93 | isUUIDValid, |
diff --git a/server/helpers/custom-validators/servers.ts b/server/helpers/custom-validators/servers.ts index d5021bf38..18c80ec8f 100644 --- a/server/helpers/custom-validators/servers.ts +++ b/server/helpers/custom-validators/servers.ts | |||
@@ -3,6 +3,7 @@ import 'express-validator' | |||
3 | 3 | ||
4 | import { isArray, exists } from './misc' | 4 | import { isArray, exists } from './misc' |
5 | import { isTestInstance } from '../core-utils' | 5 | import { isTestInstance } from '../core-utils' |
6 | import { CONSTRAINTS_FIELDS } from '../../initializers' | ||
6 | 7 | ||
7 | function isHostValid (host: string) { | 8 | function isHostValid (host: string) { |
8 | const isURLOptions = { | 9 | const isURLOptions = { |
@@ -26,9 +27,19 @@ function isEachUniqueHostValid (hosts: string[]) { | |||
26 | }) | 27 | }) |
27 | } | 28 | } |
28 | 29 | ||
30 | function isValidContactBody (value: any) { | ||
31 | return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.BODY) | ||
32 | } | ||
33 | |||
34 | function isValidContactFromName (value: any) { | ||
35 | return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.FROM_NAME) | ||
36 | } | ||
37 | |||
29 | // --------------------------------------------------------------------------- | 38 | // --------------------------------------------------------------------------- |
30 | 39 | ||
31 | export { | 40 | export { |
41 | isValidContactBody, | ||
42 | isValidContactFromName, | ||
32 | isEachUniqueHostValid, | 43 | isEachUniqueHostValid, |
33 | isHostValid | 44 | isHostValid |
34 | } | 45 | } |
diff --git a/server/helpers/custom-validators/user-notifications.ts b/server/helpers/custom-validators/user-notifications.ts new file mode 100644 index 000000000..02ea3bbc2 --- /dev/null +++ b/server/helpers/custom-validators/user-notifications.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | import { exists } from './misc' | ||
2 | import * as validator from 'validator' | ||
3 | import { UserNotificationType } from '../../../shared/models/users' | ||
4 | import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | ||
5 | |||
6 | function isUserNotificationTypeValid (value: any) { | ||
7 | return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined | ||
8 | } | ||
9 | |||
10 | function isUserNotificationSettingValid (value: any) { | ||
11 | return exists(value) && | ||
12 | validator.isInt('' + value) && ( | ||
13 | value === UserNotificationSettingValue.NONE || | ||
14 | value === UserNotificationSettingValue.WEB || | ||
15 | value === UserNotificationSettingValue.EMAIL || | ||
16 | value === (UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL) | ||
17 | ) | ||
18 | } | ||
19 | |||
20 | export { | ||
21 | isUserNotificationSettingValid, | ||
22 | isUserNotificationTypeValid | ||
23 | } | ||
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index 90fc74a48..80652b479 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -42,6 +42,14 @@ function isUserNSFWPolicyValid (value: any) { | |||
42 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 | 42 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 |
43 | } | 43 | } |
44 | 44 | ||
45 | function isUserWebTorrentEnabledValid (value: any) { | ||
46 | return isBooleanValid(value) | ||
47 | } | ||
48 | |||
49 | function isUserVideosHistoryEnabledValid (value: any) { | ||
50 | return isBooleanValid(value) | ||
51 | } | ||
52 | |||
45 | function isUserAutoPlayVideoValid (value: any) { | 53 | function isUserAutoPlayVideoValid (value: any) { |
46 | return isBooleanValid(value) | 54 | return isBooleanValid(value) |
47 | } | 55 | } |
@@ -69,6 +77,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | | |||
69 | // --------------------------------------------------------------------------- | 77 | // --------------------------------------------------------------------------- |
70 | 78 | ||
71 | export { | 79 | export { |
80 | isUserVideosHistoryEnabledValid, | ||
72 | isUserBlockedValid, | 81 | isUserBlockedValid, |
73 | isUserPasswordValid, | 82 | isUserPasswordValid, |
74 | isUserBlockedReasonValid, | 83 | isUserBlockedReasonValid, |
@@ -78,6 +87,7 @@ export { | |||
78 | isUserUsernameValid, | 87 | isUserUsernameValid, |
79 | isUserEmailVerifiedValid, | 88 | isUserEmailVerifiedValid, |
80 | isUserNSFWPolicyValid, | 89 | isUserNSFWPolicyValid, |
90 | isUserWebTorrentEnabledValid, | ||
81 | isUserAutoPlayVideoValid, | 91 | isUserAutoPlayVideoValid, |
82 | isUserDisplayNameValid, | 92 | isUserDisplayNameValid, |
83 | isUserDescriptionValid, | 93 | isUserDescriptionValid, |
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts index 177e9e86e..b33d90e18 100644 --- a/server/helpers/custom-validators/video-captions.ts +++ b/server/helpers/custom-validators/video-captions.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { CONSTRAINTS_FIELDS, VIDEO_CAPTIONS_MIMETYPE_EXT, VIDEO_LANGUAGES } from '../../initializers' | 1 | import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers' |
2 | import { exists, isFileValid } from './misc' | 2 | import { exists, isFileValid } from './misc' |
3 | import { Response } from 'express' | 3 | import { Response } from 'express' |
4 | import { VideoModel } from '../../models/video/video' | 4 | import { VideoModel } from '../../models/video/video' |
@@ -8,7 +8,7 @@ function isVideoCaptionLanguageValid (value: any) { | |||
8 | return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined | 8 | return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined |
9 | } | 9 | } |
10 | 10 | ||
11 | const videoCaptionTypes = Object.keys(VIDEO_CAPTIONS_MIMETYPE_EXT) | 11 | const videoCaptionTypes = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT) |
12 | .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream >< | 12 | .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream >< |
13 | .map(m => `(${m})`) | 13 | .map(m => `(${m})`) |
14 | const videoCaptionTypesRegex = videoCaptionTypes.join('|') | 14 | const videoCaptionTypesRegex = videoCaptionTypes.join('|') |
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts index 4d6ab1fa4..ce9e9193c 100644 --- a/server/helpers/custom-validators/video-imports.ts +++ b/server/helpers/custom-validators/video-imports.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import 'express-validator' | 1 | import 'express-validator' |
2 | import 'multer' | 2 | import 'multer' |
3 | import * as validator from 'validator' | 3 | import * as validator from 'validator' |
4 | import { CONSTRAINTS_FIELDS, TORRENT_MIMETYPE_EXT, VIDEO_IMPORT_STATES } from '../../initializers' | 4 | import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers' |
5 | import { exists, isFileValid } from './misc' | 5 | import { exists, isFileValid } from './misc' |
6 | import * as express from 'express' | 6 | import * as express from 'express' |
7 | import { VideoImportModel } from '../../models/video/video-import' | 7 | import { VideoImportModel } from '../../models/video/video-import' |
@@ -24,7 +24,7 @@ function isVideoImportStateValid (value: any) { | |||
24 | return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined | 24 | return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined |
25 | } | 25 | } |
26 | 26 | ||
27 | const videoTorrentImportTypes = Object.keys(TORRENT_MIMETYPE_EXT).map(m => `(${m})`) | 27 | const videoTorrentImportTypes = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT).map(m => `(${m})`) |
28 | const videoTorrentImportRegex = videoTorrentImportTypes.join('|') | 28 | const videoTorrentImportRegex = videoTorrentImportTypes.join('|') |
29 | function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 29 | function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { |
30 | return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true) | 30 | return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true) |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 714f7ac95..95e256b8f 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -3,12 +3,11 @@ import 'express-validator' | |||
3 | import { values } from 'lodash' | 3 | import { values } from 'lodash' |
4 | import 'multer' | 4 | import 'multer' |
5 | import * as validator from 'validator' | 5 | import * as validator from 'validator' |
6 | import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' | 6 | import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' |
7 | import { | 7 | import { |
8 | CONSTRAINTS_FIELDS, | 8 | CONSTRAINTS_FIELDS, MIMETYPES, |
9 | VIDEO_CATEGORIES, | 9 | VIDEO_CATEGORIES, |
10 | VIDEO_LICENCES, | 10 | VIDEO_LICENCES, |
11 | VIDEO_MIMETYPE_EXT, | ||
12 | VIDEO_PRIVACIES, | 11 | VIDEO_PRIVACIES, |
13 | VIDEO_RATE_TYPES, | 12 | VIDEO_RATE_TYPES, |
14 | VIDEO_STATES | 13 | VIDEO_STATES |
@@ -22,6 +21,10 @@ import { fetchVideo, VideoFetchType } from '../video' | |||
22 | 21 | ||
23 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 22 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
24 | 23 | ||
24 | function isVideoFilterValid (filter: VideoFilter) { | ||
25 | return filter === 'local' || filter === 'all-local' | ||
26 | } | ||
27 | |||
25 | function isVideoCategoryValid (value: any) { | 28 | function isVideoCategoryValid (value: any) { |
26 | return value === null || VIDEO_CATEGORIES[ value ] !== undefined | 29 | return value === null || VIDEO_CATEGORIES[ value ] !== undefined |
27 | } | 30 | } |
@@ -79,10 +82,15 @@ function isVideoRatingTypeValid (value: string) { | |||
79 | return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1 | 82 | return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1 |
80 | } | 83 | } |
81 | 84 | ||
82 | const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) | 85 | function isVideoFileExtnameValid (value: string) { |
83 | const videoFileTypesRegex = videoFileTypes.join('|') | 86 | return exists(value) && MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined |
87 | } | ||
84 | 88 | ||
85 | function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 89 | function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { |
90 | const videoFileTypesRegex = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) | ||
91 | .map(m => `(${m})`) | ||
92 | .join('|') | ||
93 | |||
86 | return isFileValid(files, videoFileTypesRegex, 'videofile', null) | 94 | return isFileValid(files, videoFileTypesRegex, 'videofile', null) |
87 | } | 95 | } |
88 | 96 | ||
@@ -217,6 +225,7 @@ export { | |||
217 | isVideoStateValid, | 225 | isVideoStateValid, |
218 | isVideoViewsValid, | 226 | isVideoViewsValid, |
219 | isVideoRatingTypeValid, | 227 | isVideoRatingTypeValid, |
228 | isVideoFileExtnameValid, | ||
220 | isVideoDurationValid, | 229 | isVideoDurationValid, |
221 | isVideoTagValid, | 230 | isVideoTagValid, |
222 | isVideoPrivacyValid, | 231 | isVideoPrivacyValid, |
@@ -225,5 +234,6 @@ export { | |||
225 | isVideoExist, | 234 | isVideoExist, |
226 | isVideoImage, | 235 | isVideoImage, |
227 | isVideoChannelOfAccountExist, | 236 | isVideoChannelOfAccountExist, |
228 | isVideoSupportValid | 237 | isVideoSupportValid, |
238 | isVideoFilterValid | ||
229 | } | 239 | } |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index 8a9cee8c5..9a72ee96d 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -2,18 +2,17 @@ import * as express from 'express' | |||
2 | import * as multer from 'multer' | 2 | import * as multer from 'multer' |
3 | import { CONFIG, REMOTE_SCHEME } from '../initializers' | 3 | import { CONFIG, REMOTE_SCHEME } from '../initializers' |
4 | import { logger } from './logger' | 4 | import { logger } from './logger' |
5 | import { User } from '../../shared/models/users' | ||
6 | import { deleteFileAsync, generateRandomString } from './utils' | 5 | import { deleteFileAsync, generateRandomString } from './utils' |
7 | import { extname } from 'path' | 6 | import { extname } from 'path' |
8 | import { isArray } from './custom-validators/misc' | 7 | import { isArray } from './custom-validators/misc' |
9 | import { UserModel } from '../models/account/user' | 8 | import { UserModel } from '../models/account/user' |
10 | 9 | ||
11 | function buildNSFWFilter (res: express.Response, paramNSFW?: string) { | 10 | function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { |
12 | if (paramNSFW === 'true') return true | 11 | if (paramNSFW === 'true') return true |
13 | if (paramNSFW === 'false') return false | 12 | if (paramNSFW === 'false') return false |
14 | if (paramNSFW === 'both') return undefined | 13 | if (paramNSFW === 'both') return undefined |
15 | 14 | ||
16 | if (res.locals.oauth) { | 15 | if (res && res.locals.oauth) { |
17 | const user: UserModel = res.locals.oauth.token.User | 16 | const user: UserModel = res.locals.oauth.token.User |
18 | 17 | ||
19 | // User does not want NSFW videos | 18 | // User does not want NSFW videos |
@@ -101,7 +100,7 @@ function createReqFiles ( | |||
101 | } | 100 | } |
102 | 101 | ||
103 | function isUserAbleToSearchRemoteURI (res: express.Response) { | 102 | function isUserAbleToSearchRemoteURI (res: express.Response) { |
104 | const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined | 103 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined |
105 | 104 | ||
106 | return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || | 105 | return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || |
107 | (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) | 106 | (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 22bc25476..133b1b03b 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { VideoResolution } from '../../shared/models/videos' | 3 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' | 4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { logger } from './logger' | 6 | import { logger } from './logger' |
7 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' | 7 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' |
@@ -29,19 +29,28 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
29 | return resolutionsEnabled | 29 | return resolutionsEnabled |
30 | } | 30 | } |
31 | 31 | ||
32 | async function getVideoFileResolution (path: string) { | 32 | async function getVideoFileSize (path: string) { |
33 | const videoStream = await getVideoFileStream(path) | 33 | const videoStream = await getVideoFileStream(path) |
34 | 34 | ||
35 | return { | 35 | return { |
36 | videoFileResolution: Math.min(videoStream.height, videoStream.width), | 36 | width: videoStream.width, |
37 | isPortraitMode: videoStream.height > videoStream.width | 37 | height: videoStream.height |
38 | } | ||
39 | } | ||
40 | |||
41 | async function getVideoFileResolution (path: string) { | ||
42 | const size = await getVideoFileSize(path) | ||
43 | |||
44 | return { | ||
45 | videoFileResolution: Math.min(size.height, size.width), | ||
46 | isPortraitMode: size.height > size.width | ||
38 | } | 47 | } |
39 | } | 48 | } |
40 | 49 | ||
41 | async function getVideoFileFPS (path: string) { | 50 | async function getVideoFileFPS (path: string) { |
42 | const videoStream = await getVideoFileStream(path) | 51 | const videoStream = await getVideoFileStream(path) |
43 | 52 | ||
44 | for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) { | 53 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { |
45 | const valuesText: string = videoStream[key] | 54 | const valuesText: string = videoStream[key] |
46 | if (!valuesText) continue | 55 | if (!valuesText) continue |
47 | 56 | ||
@@ -55,6 +64,16 @@ async function getVideoFileFPS (path: string) { | |||
55 | return 0 | 64 | return 0 |
56 | } | 65 | } |
57 | 66 | ||
67 | async function getVideoFileBitrate (path: string) { | ||
68 | return new Promise<number>((res, rej) => { | ||
69 | ffmpeg.ffprobe(path, (err, metadata) => { | ||
70 | if (err) return rej(err) | ||
71 | |||
72 | return res(metadata.format.bit_rate) | ||
73 | }) | ||
74 | }) | ||
75 | } | ||
76 | |||
58 | function getDurationFromVideoFile (path: string) { | 77 | function getDurationFromVideoFile (path: string) { |
59 | return new Promise<number>((res, rej) => { | 78 | return new Promise<number>((res, rej) => { |
60 | ffmpeg.ffprobe(path, (err, metadata) => { | 79 | ffmpeg.ffprobe(path, (err, metadata) => { |
@@ -100,64 +119,87 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima | |||
100 | type TranscodeOptions = { | 119 | type TranscodeOptions = { |
101 | inputPath: string | 120 | inputPath: string |
102 | outputPath: string | 121 | outputPath: string |
103 | resolution?: VideoResolution | 122 | resolution: VideoResolution |
104 | isPortraitMode?: boolean | 123 | isPortraitMode?: boolean |
124 | |||
125 | hlsPlaylist?: { | ||
126 | videoFilename: string | ||
127 | } | ||
105 | } | 128 | } |
106 | 129 | ||
107 | function transcode (options: TranscodeOptions) { | 130 | function transcode (options: TranscodeOptions) { |
108 | return new Promise<void>(async (res, rej) => { | 131 | return new Promise<void>(async (res, rej) => { |
109 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) | 132 | try { |
110 | .output(options.outputPath) | 133 | let fps = await getVideoFileFPS(options.inputPath) |
111 | .preset(standard) | ||
112 | |||
113 | if (CONFIG.TRANSCODING.THREADS > 0) { | ||
114 | // if we don't set any threads ffmpeg will chose automatically | ||
115 | command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | ||
116 | } | ||
117 | |||
118 | let fps = await getVideoFileFPS(options.inputPath) | ||
119 | if (options.resolution !== undefined) { | ||
120 | // '?x720' or '720x?' for example | ||
121 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` | ||
122 | command = command.size(size) | ||
123 | |||
124 | // On small/medium resolutions, limit FPS | 134 | // On small/medium resolutions, limit FPS |
125 | if ( | 135 | if ( |
136 | options.resolution !== undefined && | ||
126 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && | 137 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && |
127 | fps > VIDEO_TRANSCODING_FPS.AVERAGE | 138 | fps > VIDEO_TRANSCODING_FPS.AVERAGE |
128 | ) { | 139 | ) { |
129 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | 140 | fps = VIDEO_TRANSCODING_FPS.AVERAGE |
130 | } | 141 | } |
131 | } | ||
132 | 142 | ||
133 | if (fps) { | 143 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) |
134 | // Hard FPS limits | 144 | .output(options.outputPath) |
135 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | 145 | command = await presetH264(command, options.resolution, fps) |
136 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | ||
137 | 146 | ||
138 | command = command.withFPS(fps) | 147 | if (CONFIG.TRANSCODING.THREADS > 0) { |
139 | } | 148 | // if we don't set any threads ffmpeg will chose automatically |
149 | command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | ||
150 | } | ||
151 | |||
152 | if (options.resolution !== undefined) { | ||
153 | // '?x720' or '720x?' for example | ||
154 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` | ||
155 | command = command.size(size) | ||
156 | } | ||
157 | |||
158 | if (fps) { | ||
159 | // Hard FPS limits | ||
160 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | ||
161 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | ||
162 | |||
163 | command = command.withFPS(fps) | ||
164 | } | ||
165 | |||
166 | if (options.hlsPlaylist) { | ||
167 | const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` | ||
168 | |||
169 | command = command.outputOption('-hls_time 4') | ||
170 | .outputOption('-hls_list_size 0') | ||
171 | .outputOption('-hls_playlist_type vod') | ||
172 | .outputOption('-hls_segment_filename ' + videoPath) | ||
173 | .outputOption('-hls_segment_type fmp4') | ||
174 | .outputOption('-f hls') | ||
175 | .outputOption('-hls_flags single_file') | ||
176 | } | ||
140 | 177 | ||
141 | command | 178 | command |
142 | .on('error', (err, stdout, stderr) => { | 179 | .on('error', (err, stdout, stderr) => { |
143 | logger.error('Error in transcoding job.', { stdout, stderr }) | 180 | logger.error('Error in transcoding job.', { stdout, stderr }) |
144 | return rej(err) | 181 | return rej(err) |
145 | }) | 182 | }) |
146 | .on('end', res) | 183 | .on('end', res) |
147 | .run() | 184 | .run() |
185 | } catch (err) { | ||
186 | return rej(err) | ||
187 | } | ||
148 | }) | 188 | }) |
149 | } | 189 | } |
150 | 190 | ||
151 | // --------------------------------------------------------------------------- | 191 | // --------------------------------------------------------------------------- |
152 | 192 | ||
153 | export { | 193 | export { |
194 | getVideoFileSize, | ||
154 | getVideoFileResolution, | 195 | getVideoFileResolution, |
155 | getDurationFromVideoFile, | 196 | getDurationFromVideoFile, |
156 | generateImageFromVideoFile, | 197 | generateImageFromVideoFile, |
157 | transcode, | 198 | transcode, |
158 | getVideoFileFPS, | 199 | getVideoFileFPS, |
159 | computeResolutionsToTranscode, | 200 | computeResolutionsToTranscode, |
160 | audio | 201 | audio, |
202 | getVideoFileBitrate | ||
161 | } | 203 | } |
162 | 204 | ||
163 | // --------------------------------------------------------------------------- | 205 | // --------------------------------------------------------------------------- |
@@ -168,7 +210,7 @@ function getVideoFileStream (path: string) { | |||
168 | if (err) return rej(err) | 210 | if (err) return rej(err) |
169 | 211 | ||
170 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') | 212 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') |
171 | if (!videoStream) throw new Error('Cannot find video stream of ' + path) | 213 | if (!videoStream) return rej(new Error('Cannot find video stream of ' + path)) |
172 | 214 | ||
173 | return res(videoStream) | 215 | return res(videoStream) |
174 | }) | 216 | }) |
@@ -182,11 +224,10 @@ function getVideoFileStream (path: string) { | |||
182 | * and quality. Superfast and ultrafast will give you better | 224 | * and quality. Superfast and ultrafast will give you better |
183 | * performance, but then quality is noticeably worse. | 225 | * performance, but then quality is noticeably worse. |
184 | */ | 226 | */ |
185 | function veryfast (_ffmpeg) { | 227 | async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { |
186 | _ffmpeg | 228 | let localCommand = await presetH264(command, resolution, fps) |
187 | .preset(standard) | 229 | localCommand = localCommand.outputOption('-preset:v veryfast') |
188 | .outputOption('-preset:v veryfast') | 230 | .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ]) |
189 | .outputOption(['--aq-mode=2', '--aq-strength=1.3']) | ||
190 | /* | 231 | /* |
191 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html | 232 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html |
192 | Our target situation is closer to a livestream than a stream, | 233 | Our target situation is closer to a livestream than a stream, |
@@ -198,31 +239,39 @@ function veryfast (_ffmpeg) { | |||
198 | Make up for most of the loss of grain and macroblocking | 239 | Make up for most of the loss of grain and macroblocking |
199 | with less computing power. | 240 | with less computing power. |
200 | */ | 241 | */ |
242 | |||
243 | return localCommand | ||
201 | } | 244 | } |
202 | 245 | ||
203 | /** | 246 | /** |
204 | * A preset optimised for a stillimage audio video | 247 | * A preset optimised for a stillimage audio video |
205 | */ | 248 | */ |
206 | function audio (_ffmpeg) { | 249 | async function presetStillImageWithAudio ( |
207 | _ffmpeg | 250 | command: ffmpeg.FfmpegCommand, |
208 | .preset(veryfast) | 251 | resolution: VideoResolution, |
209 | .outputOption('-tune stillimage') | 252 | fps: number |
253 | ): Promise<ffmpeg.FfmpegCommand> { | ||
254 | let localCommand = await presetH264VeryFast(command, resolution, fps) | ||
255 | localCommand = localCommand.outputOption('-tune stillimage') | ||
256 | |||
257 | return localCommand | ||
210 | } | 258 | } |
211 | 259 | ||
212 | /** | 260 | /** |
213 | * A toolbox to play with audio | 261 | * A toolbox to play with audio |
214 | */ | 262 | */ |
215 | namespace audio { | 263 | namespace audio { |
216 | export const get = (_ffmpeg, pos: number | string = 0) => { | 264 | export const get = (option: ffmpeg.FfmpegCommand | string) => { |
217 | // without position, ffprobe considers the last input only | 265 | // without position, ffprobe considers the last input only |
218 | // we make it consider the first input only | 266 | // we make it consider the first input only |
219 | // if you pass a file path to pos, then ffprobe acts on that file directly | 267 | // if you pass a file path to pos, then ffprobe acts on that file directly |
220 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { | 268 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { |
221 | _ffmpeg.ffprobe(pos, (err,data) => { | 269 | |
270 | function parseFfprobe (err: any, data: ffmpeg.FfprobeData) { | ||
222 | if (err) return rej(err) | 271 | if (err) return rej(err) |
223 | 272 | ||
224 | if ('streams' in data) { | 273 | if ('streams' in data) { |
225 | const audioStream = data['streams'].find(stream => stream['codec_type'] === 'audio') | 274 | const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio') |
226 | if (audioStream) { | 275 | if (audioStream) { |
227 | return res({ | 276 | return res({ |
228 | absolutePath: data.format.filename, | 277 | absolutePath: data.format.filename, |
@@ -230,8 +279,15 @@ namespace audio { | |||
230 | }) | 279 | }) |
231 | } | 280 | } |
232 | } | 281 | } |
282 | |||
233 | return res({ absolutePath: data.format.filename }) | 283 | return res({ absolutePath: data.format.filename }) |
234 | }) | 284 | } |
285 | |||
286 | if (typeof option === 'string') { | ||
287 | return ffmpeg.ffprobe(option, parseFfprobe) | ||
288 | } | ||
289 | |||
290 | return option.ffprobe(parseFfprobe) | ||
235 | }) | 291 | }) |
236 | } | 292 | } |
237 | 293 | ||
@@ -273,39 +329,48 @@ namespace audio { | |||
273 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel | 329 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel |
274 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr | 330 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr |
275 | */ | 331 | */ |
276 | async function standard (_ffmpeg) { | 332 | async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { |
277 | let localFfmpeg = _ffmpeg | 333 | let localCommand = command |
278 | .format('mp4') | 334 | .format('mp4') |
279 | .videoCodec('libx264') | 335 | .videoCodec('libx264') |
280 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution | 336 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution |
281 | .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it | 337 | .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it |
282 | .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 | 338 | .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 |
339 | .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video) | ||
283 | .outputOption('-map_metadata -1') // strip all metadata | 340 | .outputOption('-map_metadata -1') // strip all metadata |
284 | .outputOption('-movflags faststart') | 341 | .outputOption('-movflags faststart') |
285 | const _audio = await audio.get(localFfmpeg) | ||
286 | 342 | ||
287 | if (!_audio.audioStream) { | 343 | const parsedAudio = await audio.get(localCommand) |
288 | return localFfmpeg.noAudio() | ||
289 | } | ||
290 | 344 | ||
291 | // we favor VBR, if a good AAC encoder is available | 345 | if (!parsedAudio.audioStream) { |
292 | if ((await checkFFmpegEncoders()).get('libfdk_aac')) { | 346 | localCommand = localCommand.noAudio() |
293 | return localFfmpeg | 347 | } else if ((await checkFFmpegEncoders()).get('libfdk_aac')) { // we favor VBR, if a good AAC encoder is available |
348 | localCommand = localCommand | ||
294 | .audioCodec('libfdk_aac') | 349 | .audioCodec('libfdk_aac') |
295 | .audioQuality(5) | 350 | .audioQuality(5) |
351 | } else { | ||
352 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | ||
353 | // of course this is far from perfect, but it might save some space in the end | ||
354 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] | ||
355 | let bitrate: number | ||
356 | if (audio.bitrate[ audioCodecName ]) { | ||
357 | localCommand = localCommand.audioCodec('aac') | ||
358 | |||
359 | bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | ||
360 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) | ||
361 | } | ||
296 | } | 362 | } |
297 | 363 | ||
298 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | 364 | // Constrained Encoding (VBV) |
299 | // of course this is far from perfect, but it might save some space in the end | 365 | // https://slhck.info/video/2017/03/01/rate-control.html |
300 | const audioCodecName = _audio.audioStream['codec_name'] | 366 | // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate |
301 | let bitrate: number | 367 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) |
302 | if (audio.bitrate[audioCodecName]) { | 368 | localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) |
303 | bitrate = audio.bitrate[audioCodecName](_audio.audioStream['bit_rate']) | ||
304 | |||
305 | if (bitrate === -1) return localFfmpeg.audioCodec('copy') | ||
306 | } | ||
307 | 369 | ||
308 | if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate) | 370 | // Keyframe interval of 2 seconds for faster seeking and resolution switching. |
371 | // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html | ||
372 | // https://superuser.com/a/908325 | ||
373 | localCommand = localCommand.outputOption(`-g ${ fps * 2 }`) | ||
309 | 374 | ||
310 | return localFfmpeg | 375 | return localCommand |
311 | } | 376 | } |
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index 3eaa674ed..e43ea3f1d 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -1,13 +1,26 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import * as sharp from 'sharp' | 2 | import * as sharp from 'sharp' |
3 | import { remove } from 'fs-extra' | 3 | import { readFile, remove } from 'fs-extra' |
4 | import { logger } from './logger' | ||
4 | 5 | ||
5 | async function processImage ( | 6 | async function processImage ( |
6 | physicalFile: { path: string }, | 7 | physicalFile: { path: string }, |
7 | destination: string, | 8 | destination: string, |
8 | newSize: { width: number, height: number } | 9 | newSize: { width: number, height: number } |
9 | ) { | 10 | ) { |
10 | await sharp(physicalFile.path) | 11 | if (physicalFile.path === destination) { |
12 | throw new Error('Sharp needs an input path different that the output path.') | ||
13 | } | ||
14 | |||
15 | logger.debug('Processing image %s to %s.', physicalFile.path, destination) | ||
16 | |||
17 | // Avoid sharp cache | ||
18 | const buf = await readFile(physicalFile.path) | ||
19 | const sharpInstance = sharp(buf) | ||
20 | |||
21 | await remove(destination) | ||
22 | |||
23 | await sharpInstance | ||
11 | .resize(newSize.width, newSize.height) | 24 | .resize(newSize.width, newSize.height) |
12 | .toFile(destination) | 25 | .toFile(destination) |
13 | 26 | ||
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 5c182961d..ab9ec077e 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts | |||
@@ -1,8 +1,14 @@ | |||
1 | import { BCRYPT_SALT_SIZE, PRIVATE_RSA_KEY_SIZE } from '../initializers' | 1 | import { Request } from 'express' |
2 | import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers' | ||
2 | import { ActorModel } from '../models/activitypub/actor' | 3 | import { ActorModel } from '../models/activitypub/actor' |
3 | import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' | 4 | import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils' |
4 | import { jsig } from './custom-jsonld-signature' | 5 | import { jsig, jsonld } from './custom-jsonld-signature' |
5 | import { logger } from './logger' | 6 | import { logger } from './logger' |
7 | import { cloneDeep } from 'lodash' | ||
8 | import { createVerify } from 'crypto' | ||
9 | import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils' | ||
10 | |||
11 | const httpSignature = require('http-signature') | ||
6 | 12 | ||
7 | async function createPrivateAndPublicKeys () { | 13 | async function createPrivateAndPublicKeys () { |
8 | logger.info('Generating a RSA key...') | 14 | logger.info('Generating a RSA key...') |
@@ -13,18 +19,57 @@ async function createPrivateAndPublicKeys () { | |||
13 | return { privateKey: key, publicKey } | 19 | return { privateKey: key, publicKey } |
14 | } | 20 | } |
15 | 21 | ||
16 | function isSignatureVerified (fromActor: ActorModel, signedDocument: object) { | 22 | // User password checks |
23 | |||
24 | function comparePassword (plainPassword: string, hashPassword: string) { | ||
25 | return bcryptComparePromise(plainPassword, hashPassword) | ||
26 | } | ||
27 | |||
28 | async function cryptPassword (password: string) { | ||
29 | const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE) | ||
30 | |||
31 | return bcryptHashPromise(password, salt) | ||
32 | } | ||
33 | |||
34 | // HTTP Signature | ||
35 | |||
36 | function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean { | ||
37 | if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) { | ||
38 | return buildDigest(rawBody.toString()) === req.headers['digest'] | ||
39 | } | ||
40 | |||
41 | return true | ||
42 | } | ||
43 | |||
44 | function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean { | ||
45 | return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true | ||
46 | } | ||
47 | |||
48 | function parseHTTPSignature (req: Request, clockSkew?: number) { | ||
49 | return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, clockSkew }) | ||
50 | } | ||
51 | |||
52 | // JSONLD | ||
53 | |||
54 | async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise<boolean> { | ||
55 | if (signedDocument.signature.type === 'RsaSignature2017') { | ||
56 | // Mastodon algorithm | ||
57 | const res = await isJsonLDRSA2017Verified(fromActor, signedDocument) | ||
58 | // Success? If no, try with our library | ||
59 | if (res === true) return true | ||
60 | } | ||
61 | |||
17 | const publicKeyObject = { | 62 | const publicKeyObject = { |
18 | '@context': jsig.SECURITY_CONTEXT_URL, | 63 | '@context': jsig.SECURITY_CONTEXT_URL, |
19 | '@id': fromActor.url, | 64 | id: fromActor.url, |
20 | '@type': 'CryptographicKey', | 65 | type: 'CryptographicKey', |
21 | owner: fromActor.url, | 66 | owner: fromActor.url, |
22 | publicKeyPem: fromActor.publicKey | 67 | publicKeyPem: fromActor.publicKey |
23 | } | 68 | } |
24 | 69 | ||
25 | const publicKeyOwnerObject = { | 70 | const publicKeyOwnerObject = { |
26 | '@context': jsig.SECURITY_CONTEXT_URL, | 71 | '@context': jsig.SECURITY_CONTEXT_URL, |
27 | '@id': fromActor.url, | 72 | id: fromActor.url, |
28 | publicKey: [ publicKeyObject ] | 73 | publicKey: [ publicKeyObject ] |
29 | } | 74 | } |
30 | 75 | ||
@@ -33,14 +78,54 @@ function isSignatureVerified (fromActor: ActorModel, signedDocument: object) { | |||
33 | publicKeyOwner: publicKeyOwnerObject | 78 | publicKeyOwner: publicKeyOwnerObject |
34 | } | 79 | } |
35 | 80 | ||
36 | return jsig.promises.verify(signedDocument, options) | 81 | return jsig.promises |
37 | .catch(err => { | 82 | .verify(signedDocument, options) |
38 | logger.error('Cannot check signature.', { err }) | 83 | .then((result: { verified: boolean }) => result.verified) |
39 | return false | 84 | .catch(err => { |
40 | }) | 85 | logger.error('Cannot check signature.', { err }) |
86 | return false | ||
87 | }) | ||
88 | } | ||
89 | |||
90 | // Backward compatibility with "other" implementations | ||
91 | async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) { | ||
92 | function hash (obj: any): Promise<any> { | ||
93 | return jsonld.promises | ||
94 | .normalize(obj, { | ||
95 | algorithm: 'URDNA2015', | ||
96 | format: 'application/n-quads' | ||
97 | }) | ||
98 | .then(res => sha256(res)) | ||
99 | } | ||
100 | |||
101 | const signatureCopy = cloneDeep(signedDocument.signature) | ||
102 | Object.assign(signatureCopy, { | ||
103 | '@context': [ | ||
104 | 'https://w3id.org/security/v1', | ||
105 | { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' } | ||
106 | ] | ||
107 | }) | ||
108 | delete signatureCopy.type | ||
109 | delete signatureCopy.id | ||
110 | delete signatureCopy.signatureValue | ||
111 | |||
112 | const docWithoutSignature = cloneDeep(signedDocument) | ||
113 | delete docWithoutSignature.signature | ||
114 | |||
115 | const [ documentHash, optionsHash ] = await Promise.all([ | ||
116 | hash(docWithoutSignature), | ||
117 | hash(signatureCopy) | ||
118 | ]) | ||
119 | |||
120 | const toVerify = optionsHash + documentHash | ||
121 | |||
122 | const verify = createVerify('RSA-SHA256') | ||
123 | verify.update(toVerify, 'utf8') | ||
124 | |||
125 | return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') | ||
41 | } | 126 | } |
42 | 127 | ||
43 | function signObject (byActor: ActorModel, data: any) { | 128 | function signJsonLDObject (byActor: ActorModel, data: any) { |
44 | const options = { | 129 | const options = { |
45 | privateKeyPem: byActor.privateKey, | 130 | privateKeyPem: byActor.privateKey, |
46 | creator: byActor.url, | 131 | creator: byActor.url, |
@@ -50,22 +135,15 @@ function signObject (byActor: ActorModel, data: any) { | |||
50 | return jsig.promises.sign(data, options) | 135 | return jsig.promises.sign(data, options) |
51 | } | 136 | } |
52 | 137 | ||
53 | function comparePassword (plainPassword: string, hashPassword: string) { | ||
54 | return bcryptComparePromise(plainPassword, hashPassword) | ||
55 | } | ||
56 | |||
57 | async function cryptPassword (password: string) { | ||
58 | const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE) | ||
59 | |||
60 | return bcryptHashPromise(password, salt) | ||
61 | } | ||
62 | |||
63 | // --------------------------------------------------------------------------- | 138 | // --------------------------------------------------------------------------- |
64 | 139 | ||
65 | export { | 140 | export { |
66 | isSignatureVerified, | 141 | isHTTPSignatureDigestValid, |
142 | parseHTTPSignature, | ||
143 | isHTTPSignatureVerified, | ||
144 | isJsonLDSignatureVerified, | ||
67 | comparePassword, | 145 | comparePassword, |
68 | createPrivateAndPublicKeys, | 146 | createPrivateAndPublicKeys, |
69 | cryptPassword, | 147 | cryptPassword, |
70 | signObject | 148 | signJsonLDObject |
71 | } | 149 | } |
diff --git a/server/helpers/regexp.ts b/server/helpers/regexp.ts new file mode 100644 index 000000000..2336654b0 --- /dev/null +++ b/server/helpers/regexp.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | // Thanks to https://regex101.com | ||
2 | function regexpCapture (str: string, regex: RegExp, maxIterations = 100) { | ||
3 | let m: RegExpExecArray | ||
4 | let i = 0 | ||
5 | let result: RegExpExecArray[] = [] | ||
6 | |||
7 | // tslint:disable:no-conditional-assignment | ||
8 | while ((m = regex.exec(str)) !== null && i < maxIterations) { | ||
9 | // This is necessary to avoid infinite loops with zero-width matches | ||
10 | if (m.index === regex.lastIndex) { | ||
11 | regex.lastIndex++ | ||
12 | } | ||
13 | |||
14 | result.push(m) | ||
15 | i++ | ||
16 | } | ||
17 | |||
18 | return result | ||
19 | } | ||
20 | |||
21 | export { | ||
22 | regexpCapture | ||
23 | } | ||
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index ee9e80404..5c6dc5e19 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -1,17 +1,19 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { createWriteStream } from 'fs-extra' | 2 | import { createWriteStream } from 'fs-extra' |
3 | import * as request from 'request' | 3 | import * as request from 'request' |
4 | import { ACTIVITY_PUB } from '../initializers' | 4 | import { ACTIVITY_PUB, CONFIG } from '../initializers' |
5 | import { processImage } from './image-utils' | ||
6 | import { join } from 'path' | ||
5 | 7 | ||
6 | function doRequest ( | 8 | function doRequest <T> ( |
7 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } | 9 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } |
8 | ): Bluebird<{ response: request.RequestResponse, body: any }> { | 10 | ): Bluebird<{ response: request.RequestResponse, body: T }> { |
9 | if (requestOptions.activityPub === true) { | 11 | if (requestOptions.activityPub === true) { |
10 | if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} | 12 | if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} |
11 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER | 13 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER |
12 | } | 14 | } |
13 | 15 | ||
14 | return new Bluebird<{ response: request.RequestResponse, body: any }>((res, rej) => { | 16 | return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => { |
15 | request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) | 17 | request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) |
16 | }) | 18 | }) |
17 | } | 19 | } |
@@ -27,9 +29,18 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U | |||
27 | }) | 29 | }) |
28 | } | 30 | } |
29 | 31 | ||
32 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { | ||
33 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) | ||
34 | await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) | ||
35 | |||
36 | const destPath = join(destDir, destName) | ||
37 | await processImage({ path: tmpPath }, destPath, size) | ||
38 | } | ||
39 | |||
30 | // --------------------------------------------------------------------------- | 40 | // --------------------------------------------------------------------------- |
31 | 41 | ||
32 | export { | 42 | export { |
33 | doRequest, | 43 | doRequest, |
34 | doRequestAndSaveToFile | 44 | doRequestAndSaveToFile, |
45 | downloadImage | ||
35 | } | 46 | } |
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 6228fec04..cb0e823c5 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -19,10 +19,7 @@ async function generateRandomString (size: number) { | |||
19 | return raw.toString('hex') | 19 | return raw.toString('hex') |
20 | } | 20 | } |
21 | 21 | ||
22 | interface FormattableToJSON { | 22 | interface FormattableToJSON { toFormattedJSON (args?: any) } |
23 | toFormattedJSON (args?: any) | ||
24 | } | ||
25 | |||
26 | function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) { | 23 | function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) { |
27 | const formattedObjects: U[] = [] | 24 | const formattedObjects: U[] = [] |
28 | 25 | ||
@@ -40,21 +37,24 @@ const getServerActor = memoizee(async function () { | |||
40 | const application = await ApplicationModel.load() | 37 | const application = await ApplicationModel.load() |
41 | if (!application) throw Error('Could not load Application from database.') | 38 | if (!application) throw Error('Could not load Application from database.') |
42 | 39 | ||
43 | return application.Account.Actor | 40 | const actor = application.Account.Actor |
41 | actor.Account = application.Account | ||
42 | |||
43 | return actor | ||
44 | }) | 44 | }) |
45 | 45 | ||
46 | function generateVideoTmpPath (target: string | ParseTorrent) { | 46 | function generateVideoImportTmpPath (target: string | ParseTorrent) { |
47 | const id = typeof target === 'string' ? target : target.infoHash | 47 | const id = typeof target === 'string' ? target : target.infoHash |
48 | 48 | ||
49 | const hash = sha256(id) | 49 | const hash = sha256(id) |
50 | return join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4') | 50 | return join(CONFIG.STORAGE.TMP_DIR, hash + '-import.mp4') |
51 | } | 51 | } |
52 | 52 | ||
53 | function getSecureTorrentName (originalName: string) { | 53 | function getSecureTorrentName (originalName: string) { |
54 | return sha256(originalName) + '.torrent' | 54 | return sha256(originalName) + '.torrent' |
55 | } | 55 | } |
56 | 56 | ||
57 | async function getVersion () { | 57 | async function getServerCommit () { |
58 | try { | 58 | try { |
59 | const tag = await execPromise2( | 59 | const tag = await execPromise2( |
60 | '[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true', | 60 | '[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true', |
@@ -74,7 +74,21 @@ async function getVersion () { | |||
74 | logger.debug('Cannot get version from git HEAD.', { err }) | 74 | logger.debug('Cannot get version from git HEAD.', { err }) |
75 | } | 75 | } |
76 | 76 | ||
77 | return require('../../../package.json').version | 77 | return '' |
78 | } | ||
79 | |||
80 | /** | ||
81 | * From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns | ||
82 | * only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does | ||
83 | * not contain a UUID, returns null. | ||
84 | */ | ||
85 | function getUUIDFromFilename (filename: string) { | ||
86 | const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ | ||
87 | const result = filename.match(regex) | ||
88 | |||
89 | if (!result || Array.isArray(result) === false) return null | ||
90 | |||
91 | return result[0] | ||
78 | } | 92 | } |
79 | 93 | ||
80 | // --------------------------------------------------------------------------- | 94 | // --------------------------------------------------------------------------- |
@@ -85,6 +99,7 @@ export { | |||
85 | getFormattedObjects, | 99 | getFormattedObjects, |
86 | getSecureTorrentName, | 100 | getSecureTorrentName, |
87 | getServerActor, | 101 | getServerActor, |
88 | getVersion, | 102 | getServerCommit, |
89 | generateVideoTmpPath | 103 | generateVideoImportTmpPath, |
104 | getUUIDFromFilename | ||
90 | } | 105 | } |
diff --git a/server/helpers/video.ts b/server/helpers/video.ts index 1bd21467d..c90fe06c7 100644 --- a/server/helpers/video.ts +++ b/server/helpers/video.ts | |||
@@ -1,10 +1,12 @@ | |||
1 | import { VideoModel } from '../models/video/video' | 1 | import { VideoModel } from '../models/video/video' |
2 | 2 | ||
3 | type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' | 3 | type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' |
4 | 4 | ||
5 | function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { | 5 | function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { |
6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) | 6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) |
7 | 7 | ||
8 | if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id) | ||
9 | |||
8 | if (fetchType === 'only-video') return VideoModel.load(id) | 10 | if (fetchType === 'only-video') return VideoModel.load(id) |
9 | 11 | ||
10 | if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) | 12 | if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) |
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts index ce35b87da..3c9a0b96a 100644 --- a/server/helpers/webtorrent.ts +++ b/server/helpers/webtorrent.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { logger } from './logger' | 1 | import { logger } from './logger' |
2 | import { generateVideoTmpPath } from './utils' | 2 | import { generateVideoImportTmpPath } from './utils' |
3 | import * as WebTorrent from 'webtorrent' | 3 | import * as WebTorrent from 'webtorrent' |
4 | import { createWriteStream, ensureDir, remove } from 'fs-extra' | 4 | import { createWriteStream, ensureDir, remove } from 'fs-extra' |
5 | import { CONFIG } from '../initializers' | 5 | import { CONFIG } from '../initializers' |
@@ -9,10 +9,10 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName | |||
9 | const id = target.magnetUri || target.torrentName | 9 | const id = target.magnetUri || target.torrentName |
10 | let timer | 10 | let timer |
11 | 11 | ||
12 | const path = generateVideoTmpPath(id) | 12 | const path = generateVideoImportTmpPath(id) |
13 | logger.info('Importing torrent video %s', id) | 13 | logger.info('Importing torrent video %s', id) |
14 | 14 | ||
15 | const directoryPath = join(CONFIG.STORAGE.VIDEOS_DIR, 'import') | 15 | const directoryPath = join(CONFIG.STORAGE.TMP_DIR, 'webtorrent') |
16 | await ensureDir(directoryPath) | 16 | await ensureDir(directoryPath) |
17 | 17 | ||
18 | return new Promise<string>((res, rej) => { | 18 | return new Promise<string>((res, rej) => { |
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index 70b4e1b78..b74351b42 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { truncate } from 'lodash' | 1 | import { truncate } from 'lodash' |
2 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' | 2 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' |
3 | import { logger } from './logger' | 3 | import { logger } from './logger' |
4 | import { generateVideoTmpPath } from './utils' | 4 | import { generateVideoImportTmpPath } from './utils' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { root } from './core-utils' | 6 | import { root } from './core-utils' |
7 | import { ensureDir, writeFile, remove } from 'fs-extra' | 7 | import { ensureDir, writeFile, remove } from 'fs-extra' |
@@ -24,10 +24,10 @@ const processOptions = { | |||
24 | 24 | ||
25 | function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> { | 25 | function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> { |
26 | return new Promise<YoutubeDLInfo>(async (res, rej) => { | 26 | return new Promise<YoutubeDLInfo>(async (res, rej) => { |
27 | const options = opts || [ '-j', '--flat-playlist' ] | 27 | const args = opts || [ '-j', '--flat-playlist' ] |
28 | 28 | ||
29 | const youtubeDL = await safeGetYoutubeDL() | 29 | const youtubeDL = await safeGetYoutubeDL() |
30 | youtubeDL.getInfo(url, options, (err, info) => { | 30 | youtubeDL.getInfo(url, args, processOptions, (err, info) => { |
31 | if (err) return rej(err) | 31 | if (err) return rej(err) |
32 | if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) | 32 | if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) |
33 | 33 | ||
@@ -40,7 +40,7 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> | |||
40 | } | 40 | } |
41 | 41 | ||
42 | function downloadYoutubeDLVideo (url: string, timeout: number) { | 42 | function downloadYoutubeDLVideo (url: string, timeout: number) { |
43 | const path = generateVideoTmpPath(url) | 43 | const path = generateVideoImportTmpPath(url) |
44 | let timer | 44 | let timer |
45 | 45 | ||
46 | logger.info('Importing youtubeDL video %s', url) | 46 | logger.info('Importing youtubeDL video %s', url) |
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index 72d846957..955d55206 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts | |||
@@ -10,6 +10,7 @@ import { getServerActor } from '../helpers/utils' | |||
10 | import { RecentlyAddedStrategy } from '../../shared/models/redundancy' | 10 | import { RecentlyAddedStrategy } from '../../shared/models/redundancy' |
11 | import { isArray } from '../helpers/custom-validators/misc' | 11 | import { isArray } from '../helpers/custom-validators/misc' |
12 | import { uniq } from 'lodash' | 12 | import { uniq } from 'lodash' |
13 | import { Emailer } from '../lib/emailer' | ||
13 | 14 | ||
14 | async function checkActivityPubUrls () { | 15 | async function checkActivityPubUrls () { |
15 | const actor = await getServerActor() | 16 | const actor = await getServerActor() |
@@ -32,9 +33,19 @@ async function checkActivityPubUrls () { | |||
32 | // Some checks on configuration files | 33 | // Some checks on configuration files |
33 | // Return an error message, or null if everything is okay | 34 | // Return an error message, or null if everything is okay |
34 | function checkConfig () { | 35 | function checkConfig () { |
35 | const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY | 36 | |
37 | if (!Emailer.isEnabled()) { | ||
38 | if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { | ||
39 | return 'Emailer is disabled but you require signup email verification.' | ||
40 | } | ||
41 | |||
42 | if (CONFIG.CONTACT_FORM.ENABLED) { | ||
43 | logger.warn('Emailer is disabled so the contact form will not work.') | ||
44 | } | ||
45 | } | ||
36 | 46 | ||
37 | // NSFW policy | 47 | // NSFW policy |
48 | const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY | ||
38 | { | 49 | { |
39 | const available = [ 'do_not_list', 'blur', 'display' ] | 50 | const available = [ 'do_not_list', 'blur', 'display' ] |
40 | if (available.indexOf(defaultNSFWPolicy) === -1) { | 51 | if (available.indexOf(defaultNSFWPolicy) === -1) { |
@@ -68,6 +79,7 @@ function checkConfig () { | |||
68 | } | 79 | } |
69 | } | 80 | } |
70 | 81 | ||
82 | // Check storage directory locations | ||
71 | if (isProdInstance()) { | 83 | if (isProdInstance()) { |
72 | const configStorage = config.get('storage') | 84 | const configStorage = config.get('storage') |
73 | for (const key of Object.keys(configStorage)) { | 85 | for (const key of Object.keys(configStorage)) { |
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 4f46d406a..29fdb263e 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -12,13 +12,14 @@ function checkMissedConfig () { | |||
12 | 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', | 12 | 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', |
13 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', | 13 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', |
14 | 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', | 14 | 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', |
15 | 'storage.redundancy', 'storage.tmp', 'storage.playlists', | ||
15 | 'log.level', | 16 | 'log.level', |
16 | 'user.video_quota', 'user.video_quota_daily', | 17 | 'user.video_quota', 'user.video_quota_daily', |
17 | 'cache.previews.size', 'admin.email', | 18 | 'cache.previews.size', 'admin.email', 'contact_form.enabled', |
18 | 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', | 19 | 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', |
19 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', | 20 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', |
20 | 'redundancy.videos.strategies', 'redundancy.videos.check_interval', | 21 | 'redundancy.videos.strategies', 'redundancy.videos.check_interval', |
21 | 'transcoding.enabled', 'transcoding.threads', | 22 | 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', |
22 | 'import.videos.http.enabled', 'import.videos.torrent.enabled', | 23 | 'import.videos.http.enabled', 'import.videos.torrent.enabled', |
23 | 'trending.videos.interval_days', | 24 | 'trending.videos.interval_days', |
24 | 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', | 25 | 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', |
@@ -77,7 +78,7 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) { | |||
77 | } | 78 | } |
78 | } | 79 | } |
79 | 80 | ||
80 | checkFFmpegEncoders() | 81 | return checkFFmpegEncoders() |
81 | } | 82 | } |
82 | 83 | ||
83 | // Optional encoders, if present, can be used to improve transcoding | 84 | // Optional encoders, if present, can be used to improve transcoding |
@@ -95,10 +96,10 @@ async function checkFFmpegEncoders (): Promise<Map<string, boolean>> { | |||
95 | supportedOptionalEncoders = new Map<string, boolean>() | 96 | supportedOptionalEncoders = new Map<string, boolean>() |
96 | 97 | ||
97 | for (const encoder of optionalEncoders) { | 98 | for (const encoder of optionalEncoders) { |
98 | supportedOptionalEncoders.set(encoder, | 99 | supportedOptionalEncoders.set(encoder, encoders[encoder] !== undefined) |
99 | encoders[encoder] !== undefined | ||
100 | ) | ||
101 | } | 100 | } |
101 | |||
102 | return supportedOptionalEncoders | ||
102 | } | 103 | } |
103 | 104 | ||
104 | // --------------------------------------------------------------------------- | 105 | // --------------------------------------------------------------------------- |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 1a3b52015..e5c4c4e63 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -3,9 +3,9 @@ import { dirname, join } from 'path' | |||
3 | import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' | 3 | import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' |
4 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 4 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
5 | import { FollowState } from '../../shared/models/actors' | 5 | import { FollowState } from '../../shared/models/actors' |
6 | import { VideoAbuseState, VideoImportState, VideoPrivacy } from '../../shared/models/videos' | 6 | import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' |
7 | // Do not use barrels, remain constants as independent as possible | 7 | // Do not use barrels, remain constants as independent as possible |
8 | import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' | 8 | import { buildPath, isTestInstance, parseDuration, parseBytes, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' |
9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
10 | import { invert } from 'lodash' | 10 | import { invert } from 'lodash' |
11 | import { CronRepeatOptions, EveryRepeatOptions } from 'bull' | 11 | import { CronRepeatOptions, EveryRepeatOptions } from 'bull' |
@@ -16,7 +16,7 @@ let config: IConfig = require('config') | |||
16 | 16 | ||
17 | // --------------------------------------------------------------------------- | 17 | // --------------------------------------------------------------------------- |
18 | 18 | ||
19 | const LAST_MIGRATION_VERSION = 275 | 19 | const LAST_MIGRATION_VERSION = 330 |
20 | 20 | ||
21 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
22 | 22 | ||
@@ -47,7 +47,12 @@ const SORTABLE_COLUMNS = { | |||
47 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ], | 47 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ], |
48 | 48 | ||
49 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ], | 49 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ], |
50 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ] | 50 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], |
51 | |||
52 | ACCOUNTS_BLOCKLIST: [ 'createdAt' ], | ||
53 | SERVERS_BLOCKLIST: [ 'createdAt' ], | ||
54 | |||
55 | USER_NOTIFICATIONS: [ 'createdAt' ] | ||
51 | } | 56 | } |
52 | 57 | ||
53 | const OAUTH_LIFETIME = { | 58 | const OAUTH_LIFETIME = { |
@@ -58,6 +63,7 @@ const OAUTH_LIFETIME = { | |||
58 | const ROUTE_CACHE_LIFETIME = { | 63 | const ROUTE_CACHE_LIFETIME = { |
59 | FEEDS: '15 minutes', | 64 | FEEDS: '15 minutes', |
60 | ROBOTS: '2 hours', | 65 | ROBOTS: '2 hours', |
66 | SITEMAP: '1 day', | ||
61 | SECURITYTXT: '2 hours', | 67 | SECURITYTXT: '2 hours', |
62 | NODEINFO: '10 minutes', | 68 | NODEINFO: '10 minutes', |
63 | DNT_POLICY: '1 week', | 69 | DNT_POLICY: '1 week', |
@@ -99,7 +105,8 @@ const JOB_ATTEMPTS: { [ id in JobType ]: number } = { | |||
99 | 'video-file': 1, | 105 | 'video-file': 1, |
100 | 'video-import': 1, | 106 | 'video-import': 1, |
101 | 'email': 5, | 107 | 'email': 5, |
102 | 'videos-views': 1 | 108 | 'videos-views': 1, |
109 | 'activitypub-refresher': 1 | ||
103 | } | 110 | } |
104 | const JOB_CONCURRENCY: { [ id in JobType ]: number } = { | 111 | const JOB_CONCURRENCY: { [ id in JobType ]: number } = { |
105 | 'activitypub-http-broadcast': 1, | 112 | 'activitypub-http-broadcast': 1, |
@@ -110,7 +117,8 @@ const JOB_CONCURRENCY: { [ id in JobType ]: number } = { | |||
110 | 'video-file': 1, | 117 | 'video-file': 1, |
111 | 'video-import': 1, | 118 | 'video-import': 1, |
112 | 'email': 5, | 119 | 'email': 5, |
113 | 'videos-views': 1 | 120 | 'videos-views': 1, |
121 | 'activitypub-refresher': 1 | ||
114 | } | 122 | } |
115 | const JOB_TTL: { [ id in JobType ]: number } = { | 123 | const JOB_TTL: { [ id in JobType ]: number } = { |
116 | 'activitypub-http-broadcast': 60000 * 10, // 10 minutes | 124 | 'activitypub-http-broadcast': 60000 * 10, // 10 minutes |
@@ -121,11 +129,12 @@ const JOB_TTL: { [ id in JobType ]: number } = { | |||
121 | 'video-file': 1000 * 3600 * 48, // 2 days, transcoding could be long | 129 | 'video-file': 1000 * 3600 * 48, // 2 days, transcoding could be long |
122 | 'video-import': 1000 * 3600 * 2, // hours | 130 | 'video-import': 1000 * 3600 * 2, // hours |
123 | 'email': 60000 * 10, // 10 minutes | 131 | 'email': 60000 * 10, // 10 minutes |
124 | 'videos-views': undefined // Unlimited | 132 | 'videos-views': undefined, // Unlimited |
133 | 'activitypub-refresher': 60000 * 10 // 10 minutes | ||
125 | } | 134 | } |
126 | const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = { | 135 | const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = { |
127 | 'videos-views': { | 136 | 'videos-views': { |
128 | cron: '1 * * * *' // At 1 minutes past the hour | 137 | cron: '1 * * * *' // At 1 minute past the hour |
129 | } | 138 | } |
130 | } | 139 | } |
131 | 140 | ||
@@ -137,7 +146,7 @@ const VIDEO_IMPORT_TIMEOUT = 1000 * 3600 // 1 hour | |||
137 | 146 | ||
138 | // 1 hour | 147 | // 1 hour |
139 | let SCHEDULER_INTERVALS_MS = { | 148 | let SCHEDULER_INTERVALS_MS = { |
140 | badActorFollow: 60000 * 60, // 1 hour | 149 | actorFollowScores: 60000 * 60, // 1 hour |
141 | removeOldJobs: 60000 * 60, // 1 hour | 150 | removeOldJobs: 60000 * 60, // 1 hour |
142 | updateVideos: 60000, // 1 minute | 151 | updateVideos: 60000, // 1 minute |
143 | youtubeDLUpdate: 60000 * 60 * 24 // 1 day | 152 | youtubeDLUpdate: 60000 * 60 * 24 // 1 day |
@@ -179,9 +188,12 @@ const CONFIG = { | |||
179 | FROM_ADDRESS: config.get<string>('smtp.from_address') | 188 | FROM_ADDRESS: config.get<string>('smtp.from_address') |
180 | }, | 189 | }, |
181 | STORAGE: { | 190 | STORAGE: { |
191 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), | ||
182 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), | 192 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), |
183 | LOG_DIR: buildPath(config.get<string>('storage.logs')), | 193 | LOG_DIR: buildPath(config.get<string>('storage.logs')), |
184 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), | 194 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), |
195 | PLAYLISTS_DIR: buildPath(config.get<string>('storage.playlists')), | ||
196 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), | ||
185 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), | 197 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), |
186 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), | 198 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), |
187 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), | 199 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), |
@@ -220,6 +232,9 @@ const CONFIG = { | |||
220 | ADMIN: { | 232 | ADMIN: { |
221 | get EMAIL () { return config.get<string>('admin.email') } | 233 | get EMAIL () { return config.get<string>('admin.email') } |
222 | }, | 234 | }, |
235 | CONTACT_FORM: { | ||
236 | get ENABLED () { return config.get<boolean>('contact_form.enabled') } | ||
237 | }, | ||
223 | SIGNUP: { | 238 | SIGNUP: { |
224 | get ENABLED () { return config.get<boolean>('signup.enabled') }, | 239 | get ENABLED () { return config.get<boolean>('signup.enabled') }, |
225 | get LIMIT () { return config.get<number>('signup.limit') }, | 240 | get LIMIT () { return config.get<number>('signup.limit') }, |
@@ -232,11 +247,12 @@ const CONFIG = { | |||
232 | } | 247 | } |
233 | }, | 248 | }, |
234 | USER: { | 249 | USER: { |
235 | get VIDEO_QUOTA () { return config.get<number>('user.video_quota') }, | 250 | get VIDEO_QUOTA () { return parseBytes(config.get<number>('user.video_quota')) }, |
236 | get VIDEO_QUOTA_DAILY () { return config.get<number>('user.video_quota_daily') } | 251 | get VIDEO_QUOTA_DAILY () { return parseBytes(config.get<number>('user.video_quota_daily')) } |
237 | }, | 252 | }, |
238 | TRANSCODING: { | 253 | TRANSCODING: { |
239 | get ENABLED () { return config.get<boolean>('transcoding.enabled') }, | 254 | get ENABLED () { return config.get<boolean>('transcoding.enabled') }, |
255 | get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') }, | ||
240 | get THREADS () { return config.get<number>('transcoding.threads') }, | 256 | get THREADS () { return config.get<number>('transcoding.threads') }, |
241 | RESOLUTIONS: { | 257 | RESOLUTIONS: { |
242 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, | 258 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, |
@@ -244,6 +260,9 @@ const CONFIG = { | |||
244 | get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, | 260 | get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, |
245 | get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') }, | 261 | get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') }, |
246 | get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') } | 262 | get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') } |
263 | }, | ||
264 | HLS: { | ||
265 | get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') } | ||
247 | } | 266 | } |
248 | }, | 267 | }, |
249 | IMPORT: { | 268 | IMPORT: { |
@@ -280,6 +299,7 @@ const CONFIG = { | |||
280 | get SECURITYTXT_CONTACT () { return config.get<string>('admin.email') } | 299 | get SECURITYTXT_CONTACT () { return config.get<string>('admin.email') } |
281 | }, | 300 | }, |
282 | SERVICES: { | 301 | SERVICES: { |
302 | get 'CSP-LOGGER' () { return config.get<string>('services.csp-logger') }, | ||
283 | TWITTER: { | 303 | TWITTER: { |
284 | get USERNAME () { return config.get<string>('services.twitter.username') }, | 304 | get USERNAME () { return config.get<string>('services.twitter.username') }, |
285 | get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') } | 305 | get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') } |
@@ -289,27 +309,27 @@ const CONFIG = { | |||
289 | 309 | ||
290 | // --------------------------------------------------------------------------- | 310 | // --------------------------------------------------------------------------- |
291 | 311 | ||
292 | const CONSTRAINTS_FIELDS = { | 312 | let CONSTRAINTS_FIELDS = { |
293 | USERS: { | 313 | USERS: { |
294 | NAME: { min: 3, max: 120 }, // Length | 314 | NAME: { min: 1, max: 120 }, // Length |
295 | DESCRIPTION: { min: 3, max: 250 }, // Length | 315 | DESCRIPTION: { min: 3, max: 1000 }, // Length |
296 | USERNAME: { min: 3, max: 20 }, // Length | 316 | USERNAME: { min: 1, max: 50 }, // Length |
297 | PASSWORD: { min: 6, max: 255 }, // Length | 317 | PASSWORD: { min: 6, max: 255 }, // Length |
298 | VIDEO_QUOTA: { min: -1 }, | 318 | VIDEO_QUOTA: { min: -1 }, |
299 | VIDEO_QUOTA_DAILY: { min: -1 }, | 319 | VIDEO_QUOTA_DAILY: { min: -1 }, |
300 | BLOCKED_REASON: { min: 3, max: 250 } // Length | 320 | BLOCKED_REASON: { min: 3, max: 250 } // Length |
301 | }, | 321 | }, |
302 | VIDEO_ABUSES: { | 322 | VIDEO_ABUSES: { |
303 | REASON: { min: 2, max: 300 }, // Length | 323 | REASON: { min: 2, max: 3000 }, // Length |
304 | MODERATION_COMMENT: { min: 2, max: 300 } // Length | 324 | MODERATION_COMMENT: { min: 2, max: 3000 } // Length |
305 | }, | 325 | }, |
306 | VIDEO_BLACKLIST: { | 326 | VIDEO_BLACKLIST: { |
307 | REASON: { min: 2, max: 300 } // Length | 327 | REASON: { min: 2, max: 300 } // Length |
308 | }, | 328 | }, |
309 | VIDEO_CHANNELS: { | 329 | VIDEO_CHANNELS: { |
310 | NAME: { min: 3, max: 120 }, // Length | 330 | NAME: { min: 1, max: 120 }, // Length |
311 | DESCRIPTION: { min: 3, max: 500 }, // Length | 331 | DESCRIPTION: { min: 3, max: 1000 }, // Length |
312 | SUPPORT: { min: 3, max: 500 }, // Length | 332 | SUPPORT: { min: 3, max: 1000 }, // Length |
313 | URL: { min: 3, max: 2000 } // Length | 333 | URL: { min: 3, max: 2000 } // Length |
314 | }, | 334 | }, |
315 | VIDEO_CAPTIONS: { | 335 | VIDEO_CAPTIONS: { |
@@ -333,19 +353,22 @@ const CONSTRAINTS_FIELDS = { | |||
333 | VIDEOS_REDUNDANCY: { | 353 | VIDEOS_REDUNDANCY: { |
334 | URL: { min: 3, max: 2000 } // Length | 354 | URL: { min: 3, max: 2000 } // Length |
335 | }, | 355 | }, |
356 | VIDEO_RATES: { | ||
357 | URL: { min: 3, max: 2000 } // Length | ||
358 | }, | ||
336 | VIDEOS: { | 359 | VIDEOS: { |
337 | NAME: { min: 3, max: 120 }, // Length | 360 | NAME: { min: 3, max: 120 }, // Length |
338 | LANGUAGE: { min: 1, max: 10 }, // Length | 361 | LANGUAGE: { min: 1, max: 10 }, // Length |
339 | TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length | 362 | TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length |
340 | DESCRIPTION: { min: 3, max: 10000 }, // Length | 363 | DESCRIPTION: { min: 3, max: 10000 }, // Length |
341 | SUPPORT: { min: 3, max: 500 }, // Length | 364 | SUPPORT: { min: 3, max: 1000 }, // Length |
342 | IMAGE: { | 365 | IMAGE: { |
343 | EXTNAME: [ '.jpg', '.jpeg' ], | 366 | EXTNAME: [ '.jpg', '.jpeg' ], |
344 | FILE_SIZE: { | 367 | FILE_SIZE: { |
345 | max: 2 * 1024 * 1024 // 2MB | 368 | max: 2 * 1024 * 1024 // 2MB |
346 | } | 369 | } |
347 | }, | 370 | }, |
348 | EXTNAME: [ '.mp4', '.ogv', '.webm' ], | 371 | EXTNAME: buildVideosExtname(), |
349 | INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 | 372 | INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 |
350 | DURATION: { min: 0 }, // Number | 373 | DURATION: { min: 0 }, // Number |
351 | TAGS: { min: 0, max: 5 }, // Number of total tags | 374 | TAGS: { min: 0, max: 5 }, // Number of total tags |
@@ -378,6 +401,10 @@ const CONSTRAINTS_FIELDS = { | |||
378 | }, | 401 | }, |
379 | VIDEO_SHARE: { | 402 | VIDEO_SHARE: { |
380 | URL: { min: 3, max: 2000 } // Length | 403 | URL: { min: 3, max: 2000 } // Length |
404 | }, | ||
405 | CONTACT_FORM: { | ||
406 | FROM_NAME: { min: 1, max: 120 }, // Length | ||
407 | BODY: { min: 3, max: 5000 } // Length | ||
381 | } | 408 | } |
382 | } | 409 | } |
383 | 410 | ||
@@ -393,7 +420,9 @@ const RATES_LIMIT = { | |||
393 | } | 420 | } |
394 | 421 | ||
395 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour | 422 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour |
396 | const VIDEO_TRANSCODING_FPS = { | 423 | let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour |
424 | |||
425 | const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { | ||
397 | MIN: 10, | 426 | MIN: 10, |
398 | AVERAGE: 30, | 427 | AVERAGE: 30, |
399 | MAX: 60, | 428 | MAX: 60, |
@@ -421,7 +450,7 @@ const VIDEO_CATEGORIES = { | |||
421 | 8: 'People', | 450 | 8: 'People', |
422 | 9: 'Comedy', | 451 | 9: 'Comedy', |
423 | 10: 'Entertainment', | 452 | 10: 'Entertainment', |
424 | 11: 'News', | 453 | 11: 'News & Politics', |
425 | 12: 'How To', | 454 | 12: 'How To', |
426 | 13: 'Education', | 455 | 13: 'Education', |
427 | 14: 'Activism', | 456 | 14: 'Activism', |
@@ -468,27 +497,31 @@ const VIDEO_ABUSE_STATES = { | |||
468 | [VideoAbuseState.ACCEPTED]: 'Accepted' | 497 | [VideoAbuseState.ACCEPTED]: 'Accepted' |
469 | } | 498 | } |
470 | 499 | ||
471 | const VIDEO_MIMETYPE_EXT = { | 500 | const MIMETYPES = { |
472 | 'video/webm': '.webm', | 501 | VIDEO: { |
473 | 'video/ogg': '.ogv', | 502 | MIMETYPE_EXT: buildVideoMimetypeExt(), |
474 | 'video/mp4': '.mp4' | 503 | EXT_MIMETYPE: null as { [ id: string ]: string } |
475 | } | 504 | }, |
476 | const VIDEO_EXT_MIMETYPE = invert(VIDEO_MIMETYPE_EXT) | 505 | IMAGE: { |
477 | 506 | MIMETYPE_EXT: { | |
478 | const IMAGE_MIMETYPE_EXT = { | 507 | 'image/png': '.png', |
479 | 'image/png': '.png', | 508 | 'image/jpg': '.jpg', |
480 | 'image/jpg': '.jpg', | 509 | 'image/jpeg': '.jpg' |
481 | 'image/jpeg': '.jpg' | 510 | } |
482 | } | 511 | }, |
483 | 512 | VIDEO_CAPTIONS: { | |
484 | const VIDEO_CAPTIONS_MIMETYPE_EXT = { | 513 | MIMETYPE_EXT: { |
485 | 'text/vtt': '.vtt', | 514 | 'text/vtt': '.vtt', |
486 | 'application/x-subrip': '.srt' | 515 | 'application/x-subrip': '.srt' |
487 | } | 516 | } |
488 | 517 | }, | |
489 | const TORRENT_MIMETYPE_EXT = { | 518 | TORRENT: { |
490 | 'application/x-bittorrent': '.torrent' | 519 | MIMETYPE_EXT: { |
520 | 'application/x-bittorrent': '.torrent' | ||
521 | } | ||
522 | } | ||
491 | } | 523 | } |
524 | MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) | ||
492 | 525 | ||
493 | // --------------------------------------------------------------------------- | 526 | // --------------------------------------------------------------------------- |
494 | 527 | ||
@@ -514,7 +547,7 @@ const ACTIVITY_PUB = { | |||
514 | COLLECTION_ITEMS_PER_PAGE: 10, | 547 | COLLECTION_ITEMS_PER_PAGE: 10, |
515 | FETCH_PAGE_LIMIT: 100, | 548 | FETCH_PAGE_LIMIT: 100, |
516 | URL_MIME_TYPES: { | 549 | URL_MIME_TYPES: { |
517 | VIDEO: Object.keys(VIDEO_MIMETYPE_EXT), | 550 | VIDEO: Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT), |
518 | TORRENT: [ 'application/x-bittorrent' ], | 551 | TORRENT: [ 'application/x-bittorrent' ], |
519 | MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] | 552 | MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] |
520 | }, | 553 | }, |
@@ -529,9 +562,15 @@ const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = { | |||
529 | APPLICATION: 'Application' | 562 | APPLICATION: 'Application' |
530 | } | 563 | } |
531 | 564 | ||
565 | const HTTP_SIGNATURE = { | ||
566 | HEADER_NAME: 'signature', | ||
567 | ALGORITHM: 'rsa-sha256', | ||
568 | HEADERS_TO_SIGN: [ '(request-target)', 'host', 'date', 'digest' ] | ||
569 | } | ||
570 | |||
532 | // --------------------------------------------------------------------------- | 571 | // --------------------------------------------------------------------------- |
533 | 572 | ||
534 | const PRIVATE_RSA_KEY_SIZE = 2048 | 573 | let PRIVATE_RSA_KEY_SIZE = 2048 |
535 | 574 | ||
536 | // Password encryption | 575 | // Password encryption |
537 | const BCRYPT_SALT_SIZE = 10 | 576 | const BCRYPT_SALT_SIZE = 10 |
@@ -554,6 +593,10 @@ const STATIC_PATHS = { | |||
554 | THUMBNAILS: '/static/thumbnails/', | 593 | THUMBNAILS: '/static/thumbnails/', |
555 | TORRENTS: '/static/torrents/', | 594 | TORRENTS: '/static/torrents/', |
556 | WEBSEED: '/static/webseed/', | 595 | WEBSEED: '/static/webseed/', |
596 | REDUNDANCY: '/static/redundancy/', | ||
597 | PLAYLISTS: { | ||
598 | HLS: '/static/playlists/hls' | ||
599 | }, | ||
557 | AVATARS: '/static/avatars/', | 600 | AVATARS: '/static/avatars/', |
558 | VIDEO_CAPTIONS: '/static/video-captions/' | 601 | VIDEO_CAPTIONS: '/static/video-captions/' |
559 | } | 602 | } |
@@ -596,6 +639,9 @@ const CACHE = { | |||
596 | } | 639 | } |
597 | } | 640 | } |
598 | 641 | ||
642 | const HLS_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.PLAYLISTS_DIR, 'hls') | ||
643 | const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | ||
644 | |||
599 | const MEMOIZE_TTL = { | 645 | const MEMOIZE_TTL = { |
600 | OVERVIEWS_SAMPLE: 1000 * 3600 * 4 // 4 hours | 646 | OVERVIEWS_SAMPLE: 1000 * 3600 * 4 // 4 hours |
601 | } | 647 | } |
@@ -635,6 +681,8 @@ const TRACKER_RATE_LIMITS = { | |||
635 | 681 | ||
636 | // Special constants for a test instance | 682 | // Special constants for a test instance |
637 | if (isTestInstance() === true) { | 683 | if (isTestInstance() === true) { |
684 | PRIVATE_RSA_KEY_SIZE = 1024 | ||
685 | |||
638 | ACTOR_FOLLOW_SCORE.BASE = 20 | 686 | ACTOR_FOLLOW_SCORE.BASE = 20 |
639 | 687 | ||
640 | REMOTE_SCHEME.HTTP = 'http' | 688 | REMOTE_SCHEME.HTTP = 'http' |
@@ -648,7 +696,7 @@ if (isTestInstance() === true) { | |||
648 | 696 | ||
649 | CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB | 697 | CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB |
650 | 698 | ||
651 | SCHEDULER_INTERVALS_MS.badActorFollow = 10000 | 699 | SCHEDULER_INTERVALS_MS.actorFollowScores = 1000 |
652 | SCHEDULER_INTERVALS_MS.removeOldJobs = 10000 | 700 | SCHEDULER_INTERVALS_MS.removeOldJobs = 10000 |
653 | SCHEDULER_INTERVALS_MS.updateVideos = 5000 | 701 | SCHEDULER_INTERVALS_MS.updateVideos = 5000 |
654 | REPEAT_JOBS['videos-views'] = { every: 5000 } | 702 | REPEAT_JOBS['videos-views'] = { every: 5000 } |
@@ -656,21 +704,24 @@ if (isTestInstance() === true) { | |||
656 | REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1 | 704 | REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1 |
657 | 705 | ||
658 | VIDEO_VIEW_LIFETIME = 1000 // 1 second | 706 | VIDEO_VIEW_LIFETIME = 1000 // 1 second |
707 | CONTACT_FORM_LIFETIME = 1000 // 1 second | ||
659 | 708 | ||
660 | JOB_ATTEMPTS['email'] = 1 | 709 | JOB_ATTEMPTS['email'] = 1 |
661 | 710 | ||
662 | CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 | 711 | CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 |
663 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 | 712 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 |
664 | ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' | 713 | ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' |
714 | |||
715 | RATES_LIMIT.LOGIN.MAX = 20 | ||
665 | } | 716 | } |
666 | 717 | ||
667 | updateWebserverConfig() | 718 | updateWebserverUrls() |
668 | 719 | ||
669 | // --------------------------------------------------------------------------- | 720 | // --------------------------------------------------------------------------- |
670 | 721 | ||
671 | export { | 722 | export { |
672 | API_VERSION, | 723 | API_VERSION, |
673 | VIDEO_CAPTIONS_MIMETYPE_EXT, | 724 | HLS_REDUNDANCY_DIRECTORY, |
674 | AVATARS_SIZE, | 725 | AVATARS_SIZE, |
675 | ACCEPT_HEADERS, | 726 | ACCEPT_HEADERS, |
676 | BCRYPT_SALT_SIZE, | 727 | BCRYPT_SALT_SIZE, |
@@ -695,10 +746,10 @@ export { | |||
695 | PRIVATE_RSA_KEY_SIZE, | 746 | PRIVATE_RSA_KEY_SIZE, |
696 | ROUTE_CACHE_LIFETIME, | 747 | ROUTE_CACHE_LIFETIME, |
697 | SORTABLE_COLUMNS, | 748 | SORTABLE_COLUMNS, |
749 | HLS_PLAYLIST_DIRECTORY, | ||
698 | FEEDS, | 750 | FEEDS, |
699 | JOB_TTL, | 751 | JOB_TTL, |
700 | NSFW_POLICY_TYPES, | 752 | NSFW_POLICY_TYPES, |
701 | TORRENT_MIMETYPE_EXT, | ||
702 | STATIC_MAX_AGE, | 753 | STATIC_MAX_AGE, |
703 | STATIC_PATHS, | 754 | STATIC_PATHS, |
704 | VIDEO_IMPORT_TIMEOUT, | 755 | VIDEO_IMPORT_TIMEOUT, |
@@ -711,7 +762,6 @@ export { | |||
711 | VIDEO_LICENCES, | 762 | VIDEO_LICENCES, |
712 | VIDEO_STATES, | 763 | VIDEO_STATES, |
713 | VIDEO_RATE_TYPES, | 764 | VIDEO_RATE_TYPES, |
714 | VIDEO_MIMETYPE_EXT, | ||
715 | VIDEO_TRANSCODING_FPS, | 765 | VIDEO_TRANSCODING_FPS, |
716 | FFMPEG_NICE, | 766 | FFMPEG_NICE, |
717 | VIDEO_ABUSE_STATES, | 767 | VIDEO_ABUSE_STATES, |
@@ -719,17 +769,18 @@ export { | |||
719 | USER_PASSWORD_RESET_LIFETIME, | 769 | USER_PASSWORD_RESET_LIFETIME, |
720 | MEMOIZE_TTL, | 770 | MEMOIZE_TTL, |
721 | USER_EMAIL_VERIFY_LIFETIME, | 771 | USER_EMAIL_VERIFY_LIFETIME, |
722 | IMAGE_MIMETYPE_EXT, | ||
723 | OVERVIEWS, | 772 | OVERVIEWS, |
724 | SCHEDULER_INTERVALS_MS, | 773 | SCHEDULER_INTERVALS_MS, |
725 | REPEAT_JOBS, | 774 | REPEAT_JOBS, |
726 | STATIC_DOWNLOAD_PATHS, | 775 | STATIC_DOWNLOAD_PATHS, |
727 | RATES_LIMIT, | 776 | RATES_LIMIT, |
728 | VIDEO_EXT_MIMETYPE, | 777 | MIMETYPES, |
729 | CRAWL_REQUEST_CONCURRENCY, | 778 | CRAWL_REQUEST_CONCURRENCY, |
730 | JOB_COMPLETED_LIFETIME, | 779 | JOB_COMPLETED_LIFETIME, |
780 | HTTP_SIGNATURE, | ||
731 | VIDEO_IMPORT_STATES, | 781 | VIDEO_IMPORT_STATES, |
732 | VIDEO_VIEW_LIFETIME, | 782 | VIDEO_VIEW_LIFETIME, |
783 | CONTACT_FORM_LIFETIME, | ||
733 | buildLanguages | 784 | buildLanguages |
734 | } | 785 | } |
735 | 786 | ||
@@ -746,16 +797,50 @@ function getLocalConfigFilePath () { | |||
746 | return join(dirname(configSources[ 0 ].name), filename + '.json') | 797 | return join(dirname(configSources[ 0 ].name), filename + '.json') |
747 | } | 798 | } |
748 | 799 | ||
749 | function updateWebserverConfig () { | 800 | function buildVideoMimetypeExt () { |
801 | const data = { | ||
802 | 'video/webm': '.webm', | ||
803 | 'video/ogg': '.ogv', | ||
804 | 'video/mp4': '.mp4' | ||
805 | } | ||
806 | |||
807 | if (CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) { | ||
808 | Object.assign(data, { | ||
809 | 'video/quicktime': '.mov', | ||
810 | 'video/x-msvideo': '.avi', | ||
811 | 'video/x-flv': '.flv', | ||
812 | 'video/x-matroska': '.mkv', | ||
813 | 'application/octet-stream': '.mkv', | ||
814 | 'video/avi': '.avi' | ||
815 | }) | ||
816 | } | ||
817 | |||
818 | return data | ||
819 | } | ||
820 | |||
821 | function updateWebserverUrls () { | ||
750 | CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT) | 822 | CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT) |
751 | CONFIG.WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP) | 823 | CONFIG.WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP) |
752 | } | 824 | } |
753 | 825 | ||
826 | function updateWebserverConfig () { | ||
827 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname() | ||
828 | |||
829 | MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt() | ||
830 | MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) | ||
831 | } | ||
832 | |||
833 | function buildVideosExtname () { | ||
834 | return CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS | ||
835 | ? [ '.mp4', '.ogv', '.webm', '.mkv', '.mov', '.avi', '.flv' ] | ||
836 | : [ '.mp4', '.ogv', '.webm' ] | ||
837 | } | ||
838 | |||
754 | function buildVideosRedundancy (objs: any[]): VideosRedundancy[] { | 839 | function buildVideosRedundancy (objs: any[]): VideosRedundancy[] { |
755 | if (!objs) return [] | 840 | if (!objs) return [] |
756 | 841 | ||
757 | return objs.map(obj => { | 842 | return objs.map(obj => { |
758 | return Object.assign(obj, { | 843 | return Object.assign({}, obj, { |
759 | minLifetime: parseDuration(obj.min_lifetime), | 844 | minLifetime: parseDuration(obj.min_lifetime), |
760 | size: bytes.parse(obj.size), | 845 | size: bytes.parse(obj.size), |
761 | minViews: obj.min_views | 846 | minViews: obj.min_views |
@@ -832,4 +917,5 @@ export function reloadConfig () { | |||
832 | config = require('config') | 917 | config = require('config') |
833 | 918 | ||
834 | updateWebserverConfig() | 919 | updateWebserverConfig() |
920 | updateWebserverUrls() | ||
835 | } | 921 | } |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 482c03b31..fe296142d 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -29,6 +29,11 @@ import { VideoViewModel } from '../models/video/video-views' | |||
29 | import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' | 29 | import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' |
30 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | 30 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' |
31 | import { UserVideoHistoryModel } from '../models/account/user-video-history' | 31 | import { UserVideoHistoryModel } from '../models/account/user-video-history' |
32 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | ||
33 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | ||
34 | import { UserNotificationModel } from '../models/account/user-notification' | ||
35 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | ||
36 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
32 | 37 | ||
33 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 38 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
34 | 39 | ||
@@ -91,7 +96,12 @@ async function initDatabaseModels (silent: boolean) { | |||
91 | VideoImportModel, | 96 | VideoImportModel, |
92 | VideoViewModel, | 97 | VideoViewModel, |
93 | VideoRedundancyModel, | 98 | VideoRedundancyModel, |
94 | UserVideoHistoryModel | 99 | UserVideoHistoryModel, |
100 | AccountBlocklistModel, | ||
101 | ServerBlocklistModel, | ||
102 | UserNotificationModel, | ||
103 | UserNotificationSettingModel, | ||
104 | VideoStreamingPlaylistModel | ||
95 | ]) | 105 | ]) |
96 | 106 | ||
97 | // Check extensions exist in the database | 107 | // Check extensions exist in the database |
@@ -115,25 +125,27 @@ export { | |||
115 | // --------------------------------------------------------------------------- | 125 | // --------------------------------------------------------------------------- |
116 | 126 | ||
117 | async function checkPostgresExtensions () { | 127 | async function checkPostgresExtensions () { |
118 | const extensions = [ | 128 | const promises = [ |
119 | 'pg_trgm', | 129 | checkPostgresExtension('pg_trgm'), |
120 | 'unaccent' | 130 | checkPostgresExtension('unaccent') |
121 | ] | 131 | ] |
122 | 132 | ||
123 | for (const extension of extensions) { | 133 | return Promise.all(promises) |
124 | const query = `SELECT true AS enabled FROM pg_available_extensions WHERE name = '${extension}' AND installed_version IS NOT NULL;` | 134 | } |
125 | const [ res ] = await sequelizeTypescript.query(query, { raw: true }) | 135 | |
136 | async function checkPostgresExtension (extension: string) { | ||
137 | const query = `SELECT true AS enabled FROM pg_available_extensions WHERE name = '${extension}' AND installed_version IS NOT NULL;` | ||
138 | const [ res ] = await sequelizeTypescript.query(query, { raw: true }) | ||
126 | 139 | ||
127 | if (!res || res.length === 0 || res[ 0 ][ 'enabled' ] !== true) { | 140 | if (!res || res.length === 0 || res[ 0 ][ 'enabled' ] !== true) { |
128 | // Try to create the extension ourself | 141 | // Try to create the extension ourself |
129 | try { | 142 | try { |
130 | await sequelizeTypescript.query(`CREATE EXTENSION ${extension};`, { raw: true }) | 143 | await sequelizeTypescript.query(`CREATE EXTENSION ${extension};`, { raw: true }) |
131 | 144 | ||
132 | } catch { | 145 | } catch { |
133 | const errorMessage = `You need to enable ${extension} extension in PostgreSQL. ` + | 146 | const errorMessage = `You need to enable ${extension} extension in PostgreSQL. ` + |
134 | `You can do so by running 'CREATE EXTENSION ${extension};' as a PostgreSQL super user in ${CONFIG.DATABASE.DBNAME} database.` | 147 | `You can do so by running 'CREATE EXTENSION ${extension};' as a PostgreSQL super user in ${CONFIG.DATABASE.DBNAME} database.` |
135 | throw new Error(errorMessage) | 148 | throw new Error(errorMessage) |
136 | } | ||
137 | } | 149 | } |
138 | } | 150 | } |
139 | } | 151 | } |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index c952ad46c..2b22e16fe 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -6,18 +6,27 @@ import { UserModel } from '../models/account/user' | |||
6 | import { ApplicationModel } from '../models/application/application' | 6 | import { ApplicationModel } from '../models/application/application' |
7 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 7 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' | 8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' |
9 | import { CACHE, CONFIG, LAST_MIGRATION_VERSION } from './constants' | 9 | import { CACHE, CONFIG, HLS_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION } from './constants' |
10 | import { sequelizeTypescript } from './database' | 10 | import { sequelizeTypescript } from './database' |
11 | import { remove, ensureDir } from 'fs-extra' | 11 | import { remove, ensureDir } from 'fs-extra' |
12 | 12 | ||
13 | async function installApplication () { | 13 | async function installApplication () { |
14 | try { | 14 | try { |
15 | await sequelizeTypescript.sync() | 15 | await Promise.all([ |
16 | await removeCacheDirectories() | 16 | // Database related |
17 | await createDirectoriesIfNotExist() | 17 | sequelizeTypescript.sync() |
18 | await createApplicationIfNotExist() | 18 | .then(() => { |
19 | await createOAuthClientIfNotExist() | 19 | return Promise.all([ |
20 | await createOAuthAdminIfNotExist() | 20 | createApplicationIfNotExist(), |
21 | createOAuthClientIfNotExist(), | ||
22 | createOAuthAdminIfNotExist() | ||
23 | ]) | ||
24 | }), | ||
25 | |||
26 | // Directories | ||
27 | removeCacheDirectories() | ||
28 | .then(() => createDirectoriesIfNotExist()) | ||
29 | ]) | ||
21 | } catch (err) { | 30 | } catch (err) { |
22 | logger.error('Cannot install application.', { err }) | 31 | logger.error('Cannot install application.', { err }) |
23 | process.exit(-1) | 32 | process.exit(-1) |
@@ -64,6 +73,9 @@ function createDirectoriesIfNotExist () { | |||
64 | tasks.push(ensureDir(dir)) | 73 | tasks.push(ensureDir(dir)) |
65 | } | 74 | } |
66 | 75 | ||
76 | // Playlist directories | ||
77 | tasks.push(ensureDir(HLS_PLAYLIST_DIRECTORY)) | ||
78 | |||
67 | return Promise.all(tasks) | 79 | return Promise.all(tasks) |
68 | } | 80 | } |
69 | 81 | ||
diff --git a/server/initializers/migrations/0120-video-null.ts b/server/initializers/migrations/0120-video-null.ts index 63f3984dd..6d253f04f 100644 --- a/server/initializers/migrations/0120-video-null.ts +++ b/server/initializers/migrations/0120-video-null.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | 2 | ||
4 | async function up (utils: { | 3 | async function up (utils: { |
5 | transaction: Sequelize.Transaction, | 4 | transaction: Sequelize.Transaction, |
@@ -28,7 +27,7 @@ async function up (utils: { | |||
28 | 27 | ||
29 | { | 28 | { |
30 | const data = { | 29 | const data = { |
31 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), | 30 | type: Sequelize.STRING(10000), |
32 | allowNull: true, | 31 | allowNull: true, |
33 | defaultValue: null | 32 | defaultValue: null |
34 | } | 33 | } |
diff --git a/server/initializers/migrations/0195-support.ts b/server/initializers/migrations/0195-support.ts index 8722a5f22..3b9eabe79 100644 --- a/server/initializers/migrations/0195-support.ts +++ b/server/initializers/migrations/0195-support.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../index' | ||
3 | 2 | ||
4 | async function up (utils: { | 3 | async function up (utils: { |
5 | transaction: Sequelize.Transaction, | 4 | transaction: Sequelize.Transaction, |
@@ -8,7 +7,7 @@ async function up (utils: { | |||
8 | }): Promise<void> { | 7 | }): Promise<void> { |
9 | { | 8 | { |
10 | const data = { | 9 | const data = { |
11 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.SUPPORT.max), | 10 | type: Sequelize.STRING(500), |
12 | allowNull: true, | 11 | allowNull: true, |
13 | defaultValue: null | 12 | defaultValue: null |
14 | } | 13 | } |
@@ -17,7 +16,7 @@ async function up (utils: { | |||
17 | 16 | ||
18 | { | 17 | { |
19 | const data = { | 18 | const data = { |
20 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max), | 19 | type: Sequelize.STRING(500), |
21 | allowNull: true, | 20 | allowNull: true, |
22 | defaultValue: null | 21 | defaultValue: null |
23 | } | 22 | } |
@@ -26,7 +25,7 @@ async function up (utils: { | |||
26 | 25 | ||
27 | { | 26 | { |
28 | const data = { | 27 | const data = { |
29 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max), | 28 | type: Sequelize.STRING(250), |
30 | allowNull: true, | 29 | allowNull: true, |
31 | defaultValue: null | 30 | defaultValue: null |
32 | } | 31 | } |
@@ -35,7 +34,7 @@ async function up (utils: { | |||
35 | 34 | ||
36 | { | 35 | { |
37 | const data = { | 36 | const data = { |
38 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), | 37 | type: Sequelize.STRING(10000), |
39 | allowNull: true, | 38 | allowNull: true, |
40 | defaultValue: null | 39 | defaultValue: null |
41 | } | 40 | } |
diff --git a/server/initializers/migrations/0245-user-blocked.ts b/server/initializers/migrations/0245-user-blocked.ts index 5a04ecd2b..19c7d5b9c 100644 --- a/server/initializers/migrations/0245-user-blocked.ts +++ b/server/initializers/migrations/0245-user-blocked.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | 2 | ||
4 | async function up (utils: { | 3 | async function up (utils: { |
5 | transaction: Sequelize.Transaction | 4 | transaction: Sequelize.Transaction |
@@ -31,7 +30,7 @@ async function up (utils: { | |||
31 | 30 | ||
32 | { | 31 | { |
33 | const data = { | 32 | const data = { |
34 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON.max), | 33 | type: Sequelize.STRING(250), |
35 | allowNull: true, | 34 | allowNull: true, |
36 | defaultValue: null | 35 | defaultValue: null |
37 | } | 36 | } |
diff --git a/server/initializers/migrations/0250-video-abuse-state.ts b/server/initializers/migrations/0250-video-abuse-state.ts index acb668ae1..50de25182 100644 --- a/server/initializers/migrations/0250-video-abuse-state.ts +++ b/server/initializers/migrations/0250-video-abuse-state.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | import { VideoAbuseState } from '../../../shared/models/videos' | 2 | import { VideoAbuseState } from '../../../shared/models/videos' |
4 | 3 | ||
5 | async function up (utils: { | 4 | async function up (utils: { |
@@ -32,7 +31,7 @@ async function up (utils: { | |||
32 | 31 | ||
33 | { | 32 | { |
34 | const data = { | 33 | const data = { |
35 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max), | 34 | type: Sequelize.STRING(300), |
36 | allowNull: true, | 35 | allowNull: true, |
37 | defaultValue: null | 36 | defaultValue: null |
38 | } | 37 | } |
diff --git a/server/initializers/migrations/0255-video-blacklist-reason.ts b/server/initializers/migrations/0255-video-blacklist-reason.ts index a380e620e..69d6efb9e 100644 --- a/server/initializers/migrations/0255-video-blacklist-reason.ts +++ b/server/initializers/migrations/0255-video-blacklist-reason.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | import { VideoAbuseState } from '../../../shared/models/videos' | 2 | import { VideoAbuseState } from '../../../shared/models/videos' |
4 | 3 | ||
5 | async function up (utils: { | 4 | async function up (utils: { |
@@ -10,7 +9,7 @@ async function up (utils: { | |||
10 | 9 | ||
11 | { | 10 | { |
12 | const data = { | 11 | const data = { |
13 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max), | 12 | type: Sequelize.STRING(300), |
14 | allowNull: true, | 13 | allowNull: true, |
15 | defaultValue: null | 14 | defaultValue: null |
16 | } | 15 | } |
diff --git a/server/initializers/migrations/0260-upload-quota-daily.ts b/server/initializers/migrations/0260-upload-quota-daily.ts index d25154ba6..cbbe391ef 100644 --- a/server/initializers/migrations/0260-upload-quota-daily.ts +++ b/server/initializers/migrations/0260-upload-quota-daily.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | 2 | ||
4 | async function up (utils: { | 3 | async function up (utils: { |
5 | transaction: Sequelize.Transaction | 4 | transaction: Sequelize.Transaction |
diff --git a/server/initializers/migrations/0275-video-file-unique.ts b/server/initializers/migrations/0275-video-file-unique.ts index fd89188c0..e321ecb04 100644 --- a/server/initializers/migrations/0275-video-file-unique.ts +++ b/server/initializers/migrations/0275-video-file-unique.ts | |||
@@ -5,6 +5,12 @@ async function up (utils: { | |||
5 | queryInterface: Sequelize.QueryInterface | 5 | queryInterface: Sequelize.QueryInterface |
6 | sequelize: Sequelize.Sequelize | 6 | sequelize: Sequelize.Sequelize |
7 | }): Promise<any> { | 7 | }): Promise<any> { |
8 | // Delete duplicated keys | ||
9 | { | ||
10 | const query = 'DELETE FROM "server" s1 USING "server" s2 WHERE s1.id < s2.id AND s1."host" = s2."host"' | ||
11 | await utils.sequelize.query(query) | ||
12 | } | ||
13 | |||
8 | { | 14 | { |
9 | const query = 'DELETE FROM "videoFile" vf1 USING "videoFile" vf2 WHERE vf1.id < vf2.id ' + | 15 | const query = 'DELETE FROM "videoFile" vf1 USING "videoFile" vf2 WHERE vf1.id < vf2.id ' + |
10 | 'AND vf1."videoId" = vf2."videoId" AND vf1.resolution = vf2.resolution AND vf1.fps IS NULL' | 16 | 'AND vf1."videoId" = vf2."videoId" AND vf1.resolution = vf2.resolution AND vf1.fps IS NULL' |
diff --git a/server/initializers/migrations/0280-webtorrent-policy-user.ts b/server/initializers/migrations/0280-webtorrent-policy-user.ts new file mode 100644 index 000000000..e6488356a --- /dev/null +++ b/server/initializers/migrations/0280-webtorrent-policy-user.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<any> { | ||
8 | { | ||
9 | const data = { | ||
10 | type: Sequelize.BOOLEAN, | ||
11 | allowNull: false, | ||
12 | defaultValue: true | ||
13 | } | ||
14 | |||
15 | await utils.queryInterface.addColumn('user', 'webTorrentEnabled', data) | ||
16 | } | ||
17 | |||
18 | } | ||
19 | |||
20 | async function down (utils: { | ||
21 | transaction: Sequelize.Transaction | ||
22 | queryInterface: Sequelize.QueryInterface | ||
23 | sequelize: Sequelize.Sequelize | ||
24 | }): Promise<any> { | ||
25 | await utils.queryInterface.removeColumn('user', 'webTorrentEnabled') | ||
26 | } | ||
27 | |||
28 | export { up, down } | ||
diff --git a/server/initializers/migrations/0285-description-support.ts b/server/initializers/migrations/0285-description-support.ts new file mode 100644 index 000000000..85ef4ef39 --- /dev/null +++ b/server/initializers/migrations/0285-description-support.ts | |||
@@ -0,0 +1,53 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.STRING(1000), | ||
12 | allowNull: true, | ||
13 | defaultValue: null | ||
14 | } | ||
15 | await utils.queryInterface.changeColumn('video', 'support', data) | ||
16 | } | ||
17 | |||
18 | { | ||
19 | const data = { | ||
20 | type: Sequelize.STRING(1000), | ||
21 | allowNull: true, | ||
22 | defaultValue: null | ||
23 | } | ||
24 | await utils.queryInterface.changeColumn('videoChannel', 'support', data) | ||
25 | } | ||
26 | |||
27 | { | ||
28 | const data = { | ||
29 | type: Sequelize.STRING(1000), | ||
30 | allowNull: true, | ||
31 | defaultValue: null | ||
32 | } | ||
33 | await utils.queryInterface.changeColumn('videoChannel', 'description', data) | ||
34 | } | ||
35 | |||
36 | { | ||
37 | const data = { | ||
38 | type: Sequelize.STRING(1000), | ||
39 | allowNull: true, | ||
40 | defaultValue: null | ||
41 | } | ||
42 | await utils.queryInterface.changeColumn('account', 'description', data) | ||
43 | } | ||
44 | } | ||
45 | |||
46 | function down (options) { | ||
47 | throw new Error('Not implemented.') | ||
48 | } | ||
49 | |||
50 | export { | ||
51 | up, | ||
52 | down | ||
53 | } | ||
diff --git a/server/initializers/migrations/0290-account-video-rate-url.ts b/server/initializers/migrations/0290-account-video-rate-url.ts new file mode 100644 index 000000000..bdabf2929 --- /dev/null +++ b/server/initializers/migrations/0290-account-video-rate-url.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.STRING(2000), | ||
12 | allowNull: true | ||
13 | } | ||
14 | |||
15 | await utils.queryInterface.addColumn('accountVideoRate', 'url', data) | ||
16 | } | ||
17 | |||
18 | { | ||
19 | const builtUrlQuery = `SELECT "actor"."url" || '/' || "accountVideoRate"."type" || 's/' || "videoId" ` + | ||
20 | 'FROM "accountVideoRate" ' + | ||
21 | 'INNER JOIN account ON account.id = "accountVideoRate"."accountId" ' + | ||
22 | 'INNER JOIN actor ON actor.id = account."actorId" ' + | ||
23 | 'WHERE "base".id = "accountVideoRate".id' | ||
24 | |||
25 | const query = 'UPDATE "accountVideoRate" base SET "url" = (' + builtUrlQuery + ') WHERE "url" IS NULL' | ||
26 | await utils.sequelize.query(query) | ||
27 | } | ||
28 | |||
29 | { | ||
30 | const data = { | ||
31 | type: Sequelize.STRING(2000), | ||
32 | allowNull: false, | ||
33 | defaultValue: null | ||
34 | } | ||
35 | await utils.queryInterface.changeColumn('accountVideoRate', 'url', data) | ||
36 | } | ||
37 | } | ||
38 | |||
39 | function down (options) { | ||
40 | throw new Error('Not implemented.') | ||
41 | } | ||
42 | |||
43 | export { | ||
44 | up, | ||
45 | down | ||
46 | } | ||
diff --git a/server/initializers/migrations/0295-video-file-extname.ts b/server/initializers/migrations/0295-video-file-extname.ts new file mode 100644 index 000000000..dbf249f66 --- /dev/null +++ b/server/initializers/migrations/0295-video-file-extname.ts | |||
@@ -0,0 +1,49 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | await utils.queryInterface.renameColumn('videoFile', 'extname', 'extname_old') | ||
11 | } | ||
12 | |||
13 | { | ||
14 | const data = { | ||
15 | type: Sequelize.STRING, | ||
16 | defaultValue: null, | ||
17 | allowNull: true | ||
18 | } | ||
19 | |||
20 | await utils.queryInterface.addColumn('videoFile', 'extname', data) | ||
21 | } | ||
22 | |||
23 | { | ||
24 | const query = 'UPDATE "videoFile" SET "extname" = "extname_old"::text' | ||
25 | await utils.sequelize.query(query) | ||
26 | } | ||
27 | |||
28 | { | ||
29 | const data = { | ||
30 | type: Sequelize.STRING, | ||
31 | defaultValue: null, | ||
32 | allowNull: false | ||
33 | } | ||
34 | await utils.queryInterface.changeColumn('videoFile', 'extname', data) | ||
35 | } | ||
36 | |||
37 | { | ||
38 | await utils.queryInterface.removeColumn('videoFile', 'extname_old') | ||
39 | } | ||
40 | } | ||
41 | |||
42 | function down (options) { | ||
43 | throw new Error('Not implemented.') | ||
44 | } | ||
45 | |||
46 | export { | ||
47 | up, | ||
48 | down | ||
49 | } | ||
diff --git a/server/initializers/migrations/0300-user-videos-history-enabled.ts b/server/initializers/migrations/0300-user-videos-history-enabled.ts new file mode 100644 index 000000000..aa5fc21fb --- /dev/null +++ b/server/initializers/migrations/0300-user-videos-history-enabled.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.BOOLEAN, | ||
12 | allowNull: false, | ||
13 | defaultValue: true | ||
14 | } | ||
15 | |||
16 | await utils.queryInterface.addColumn('user', 'videosHistoryEnabled', data) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | function down (options) { | ||
21 | throw new Error('Not implemented.') | ||
22 | } | ||
23 | |||
24 | export { | ||
25 | up, | ||
26 | down | ||
27 | } | ||
diff --git a/server/initializers/migrations/0305-fix-unfederated-videos.ts b/server/initializers/migrations/0305-fix-unfederated-videos.ts new file mode 100644 index 000000000..be206601f --- /dev/null +++ b/server/initializers/migrations/0305-fix-unfederated-videos.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const query = `INSERT INTO "videoShare" (url, "actorId", "videoId", "createdAt", "updatedAt") ` + | ||
11 | `(` + | ||
12 | `SELECT ` + | ||
13 | `video.url || '/announces/' || "videoChannel"."actorId" as url, ` + | ||
14 | `"videoChannel"."actorId" AS "actorId", ` + | ||
15 | `"video"."id" AS "videoId", ` + | ||
16 | `NOW() AS "createdAt", ` + | ||
17 | `NOW() AS "updatedAt" ` + | ||
18 | `FROM video ` + | ||
19 | `INNER JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id" ` + | ||
20 | `WHERE "video"."remote" = false AND "video"."privacy" != 3 AND "video"."state" = 1` + | ||
21 | `) ` + | ||
22 | `ON CONFLICT DO NOTHING` | ||
23 | |||
24 | await utils.sequelize.query(query) | ||
25 | } | ||
26 | |||
27 | { | ||
28 | const query = `INSERT INTO "videoShare" (url, "actorId", "videoId", "createdAt", "updatedAt") ` + | ||
29 | `(` + | ||
30 | `SELECT ` + | ||
31 | `video.url || '/announces/' || (SELECT id FROM actor WHERE "preferredUsername" = 'peertube' ORDER BY id ASC LIMIT 1) as url, ` + | ||
32 | `(SELECT id FROM actor WHERE "preferredUsername" = 'peertube' ORDER BY id ASC LIMIT 1) AS "actorId", ` + | ||
33 | `"video"."id" AS "videoId", ` + | ||
34 | `NOW() AS "createdAt", ` + | ||
35 | `NOW() AS "updatedAt" ` + | ||
36 | `FROM video ` + | ||
37 | `WHERE "video"."remote" = false AND "video"."privacy" != 3 AND "video"."state" = 1` + | ||
38 | `) ` + | ||
39 | `ON CONFLICT DO NOTHING` | ||
40 | |||
41 | await utils.sequelize.query(query) | ||
42 | } | ||
43 | } | ||
44 | |||
45 | function down (options) { | ||
46 | throw new Error('Not implemented.') | ||
47 | } | ||
48 | |||
49 | export { | ||
50 | up, | ||
51 | down | ||
52 | } | ||
diff --git a/server/initializers/migrations/0310-drop-unused-video-indexes.ts b/server/initializers/migrations/0310-drop-unused-video-indexes.ts new file mode 100644 index 000000000..d51f430c0 --- /dev/null +++ b/server/initializers/migrations/0310-drop-unused-video-indexes.ts | |||
@@ -0,0 +1,32 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | const indexNames = [ | ||
10 | 'video_category', | ||
11 | 'video_licence', | ||
12 | 'video_nsfw', | ||
13 | 'video_language', | ||
14 | 'video_wait_transcoding', | ||
15 | 'video_state', | ||
16 | 'video_remote', | ||
17 | 'video_likes' | ||
18 | ] | ||
19 | |||
20 | for (const indexName of indexNames) { | ||
21 | await utils.sequelize.query('DROP INDEX IF EXISTS "' + indexName + '";') | ||
22 | } | ||
23 | } | ||
24 | |||
25 | function down (options) { | ||
26 | throw new Error('Not implemented.') | ||
27 | } | ||
28 | |||
29 | export { | ||
30 | up, | ||
31 | down | ||
32 | } | ||
diff --git a/server/initializers/migrations/0315-user-notifications.ts b/server/initializers/migrations/0315-user-notifications.ts new file mode 100644 index 000000000..8284c58a0 --- /dev/null +++ b/server/initializers/migrations/0315-user-notifications.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | |||
9 | { | ||
10 | const query = ` | ||
11 | CREATE TABLE IF NOT EXISTS "userNotificationSetting" ("id" SERIAL, | ||
12 | "newVideoFromSubscription" INTEGER NOT NULL DEFAULT NULL, | ||
13 | "newCommentOnMyVideo" INTEGER NOT NULL DEFAULT NULL, | ||
14 | "videoAbuseAsModerator" INTEGER NOT NULL DEFAULT NULL, | ||
15 | "blacklistOnMyVideo" INTEGER NOT NULL DEFAULT NULL, | ||
16 | "myVideoPublished" INTEGER NOT NULL DEFAULT NULL, | ||
17 | "myVideoImportFinished" INTEGER NOT NULL DEFAULT NULL, | ||
18 | "newUserRegistration" INTEGER NOT NULL DEFAULT NULL, | ||
19 | "newFollow" INTEGER NOT NULL DEFAULT NULL, | ||
20 | "commentMention" INTEGER NOT NULL DEFAULT NULL, | ||
21 | "userId" INTEGER REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
22 | "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
23 | "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
24 | PRIMARY KEY ("id")) | ||
25 | ` | ||
26 | await utils.sequelize.query(query) | ||
27 | } | ||
28 | |||
29 | { | ||
30 | const query = 'INSERT INTO "userNotificationSetting" ' + | ||
31 | '("newVideoFromSubscription", "newCommentOnMyVideo", "videoAbuseAsModerator", "blacklistOnMyVideo", ' + | ||
32 | '"myVideoPublished", "myVideoImportFinished", "newUserRegistration", "newFollow", "commentMention", ' + | ||
33 | '"userId", "createdAt", "updatedAt") ' + | ||
34 | '(SELECT 1, 1, 3, 3, 1, 1, 1, 1, 1, id, NOW(), NOW() FROM "user")' | ||
35 | |||
36 | await utils.sequelize.query(query) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | function down (options) { | ||
41 | throw new Error('Not implemented.') | ||
42 | } | ||
43 | |||
44 | export { | ||
45 | up, | ||
46 | down | ||
47 | } | ||
diff --git a/server/initializers/migrations/0320-blacklist-unfederate.ts b/server/initializers/migrations/0320-blacklist-unfederate.ts new file mode 100644 index 000000000..6fb7bbb90 --- /dev/null +++ b/server/initializers/migrations/0320-blacklist-unfederate.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | |||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.BOOLEAN, | ||
12 | allowNull: false, | ||
13 | defaultValue: false | ||
14 | } | ||
15 | |||
16 | await utils.queryInterface.addColumn('videoBlacklist', 'unfederated', data) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | function down (options) { | ||
21 | throw new Error('Not implemented.') | ||
22 | } | ||
23 | |||
24 | export { | ||
25 | up, | ||
26 | down | ||
27 | } | ||
diff --git a/server/initializers/migrations/0325-video-abuse-fields.ts b/server/initializers/migrations/0325-video-abuse-fields.ts new file mode 100644 index 000000000..fca6d666f --- /dev/null +++ b/server/initializers/migrations/0325-video-abuse-fields.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | |||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.STRING(3000), | ||
12 | allowNull: false, | ||
13 | defaultValue: null | ||
14 | } | ||
15 | |||
16 | await utils.queryInterface.changeColumn('videoAbuse', 'reason', data) | ||
17 | } | ||
18 | |||
19 | { | ||
20 | const data = { | ||
21 | type: Sequelize.STRING(3000), | ||
22 | allowNull: true, | ||
23 | defaultValue: null | ||
24 | } | ||
25 | |||
26 | await utils.queryInterface.changeColumn('videoAbuse', 'moderationComment', data) | ||
27 | } | ||
28 | } | ||
29 | |||
30 | function down (options) { | ||
31 | throw new Error('Not implemented.') | ||
32 | } | ||
33 | |||
34 | export { | ||
35 | up, | ||
36 | down | ||
37 | } | ||
diff --git a/server/initializers/migrations/0330-video-streaming-playlist.ts b/server/initializers/migrations/0330-video-streaming-playlist.ts new file mode 100644 index 000000000..c85a762ab --- /dev/null +++ b/server/initializers/migrations/0330-video-streaming-playlist.ts | |||
@@ -0,0 +1,51 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | |||
9 | { | ||
10 | const query = ` | ||
11 | CREATE TABLE IF NOT EXISTS "videoStreamingPlaylist" | ||
12 | ( | ||
13 | "id" SERIAL, | ||
14 | "type" INTEGER NOT NULL, | ||
15 | "playlistUrl" VARCHAR(2000) NOT NULL, | ||
16 | "p2pMediaLoaderInfohashes" VARCHAR(255)[] NOT NULL, | ||
17 | "segmentsSha256Url" VARCHAR(255) NOT NULL, | ||
18 | "videoId" INTEGER NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
19 | "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
20 | "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
21 | PRIMARY KEY ("id") | ||
22 | );` | ||
23 | await utils.sequelize.query(query) | ||
24 | } | ||
25 | |||
26 | { | ||
27 | const data = { | ||
28 | type: Sequelize.INTEGER, | ||
29 | allowNull: true, | ||
30 | defaultValue: null | ||
31 | } | ||
32 | |||
33 | await utils.queryInterface.changeColumn('videoRedundancy', 'videoFileId', data) | ||
34 | } | ||
35 | |||
36 | { | ||
37 | const query = 'ALTER TABLE "videoRedundancy" ADD COLUMN "videoStreamingPlaylistId" INTEGER NULL ' + | ||
38 | 'REFERENCES "videoStreamingPlaylist" ("id") ON DELETE CASCADE ON UPDATE CASCADE' | ||
39 | |||
40 | await utils.sequelize.query(query) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | function down (options) { | ||
45 | throw new Error('Not implemented.') | ||
46 | } | ||
47 | |||
48 | export { | ||
49 | up, | ||
50 | down | ||
51 | } | ||
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 45dd4443d..a3f379b76 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -1,19 +1,18 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { join } from 'path' | ||
3 | import { Transaction } from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
4 | import * as url from 'url' | 3 | import * as url from 'url' |
5 | import * as uuidv4 from 'uuid/v4' | 4 | import * as uuidv4 from 'uuid/v4' |
6 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' | 5 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' |
7 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 6 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' |
8 | import { getActorUrl } from '../../helpers/activitypub' | 7 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
9 | import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' | 8 | import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' |
10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
11 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' | 10 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' |
12 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
13 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | 12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' |
14 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 13 | import { doRequest, downloadImage } from '../../helpers/requests' |
15 | import { getUrlFromWebfinger } from '../../helpers/webfinger' | 14 | import { getUrlFromWebfinger } from '../../helpers/webfinger' |
16 | import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' | 15 | import { AVATARS_SIZE, CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers' |
17 | import { AccountModel } from '../../models/account/account' | 16 | import { AccountModel } from '../../models/account/account' |
18 | import { ActorModel } from '../../models/activitypub/actor' | 17 | import { ActorModel } from '../../models/activitypub/actor' |
19 | import { AvatarModel } from '../../models/avatar/avatar' | 18 | import { AvatarModel } from '../../models/avatar/avatar' |
@@ -43,7 +42,7 @@ async function getOrCreateActorAndServerAndModel ( | |||
43 | recurseIfNeeded = true, | 42 | recurseIfNeeded = true, |
44 | updateCollections = false | 43 | updateCollections = false |
45 | ) { | 44 | ) { |
46 | const actorUrl = getActorUrl(activityActor) | 45 | const actorUrl = getAPId(activityActor) |
47 | let created = false | 46 | let created = false |
48 | 47 | ||
49 | let actor = await fetchActorByUrl(actorUrl, fetchType) | 48 | let actor = await fetchActorByUrl(actorUrl, fetchType) |
@@ -65,8 +64,12 @@ async function getOrCreateActorAndServerAndModel ( | |||
65 | const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person') | 64 | const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person') |
66 | if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url) | 65 | if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url) |
67 | 66 | ||
67 | if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) { | ||
68 | throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`) | ||
69 | } | ||
70 | |||
68 | try { | 71 | try { |
69 | // Assert we don't recurse another time | 72 | // Don't recurse another time |
70 | ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false) | 73 | ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false) |
71 | } catch (err) { | 74 | } catch (err) { |
72 | logger.error('Cannot get or create account attributed to video channel ' + actor.url) | 75 | logger.error('Cannot get or create account attributed to video channel ' + actor.url) |
@@ -168,18 +171,13 @@ async function fetchActorTotalItems (url: string) { | |||
168 | 171 | ||
169 | async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { | 172 | async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { |
170 | if ( | 173 | if ( |
171 | actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && | 174 | actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && |
172 | isActivityPubUrlValid(actorJSON.icon.url) | 175 | isActivityPubUrlValid(actorJSON.icon.url) |
173 | ) { | 176 | ) { |
174 | const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] | 177 | const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] |
175 | 178 | ||
176 | const avatarName = uuidv4() + extension | 179 | const avatarName = uuidv4() + extension |
177 | const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) | 180 | await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE) |
178 | |||
179 | await doRequestAndSaveToFile({ | ||
180 | method: 'GET', | ||
181 | uri: actorJSON.icon.url | ||
182 | }, destPath) | ||
183 | 181 | ||
184 | return avatarName | 182 | return avatarName |
185 | } | 183 | } |
@@ -203,6 +201,69 @@ async function addFetchOutboxJob (actor: ActorModel) { | |||
203 | return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | 201 | return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) |
204 | } | 202 | } |
205 | 203 | ||
204 | async function refreshActorIfNeeded ( | ||
205 | actorArg: ActorModel, | ||
206 | fetchedType: ActorFetchByUrlType | ||
207 | ): Promise<{ actor: ActorModel, refreshed: boolean }> { | ||
208 | if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false } | ||
209 | |||
210 | // We need more attributes | ||
211 | const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url) | ||
212 | |||
213 | try { | ||
214 | let actorUrl: string | ||
215 | try { | ||
216 | actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) | ||
217 | } catch (err) { | ||
218 | logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err) | ||
219 | actorUrl = actor.url | ||
220 | } | ||
221 | |||
222 | const { result, statusCode } = await fetchRemoteActor(actorUrl) | ||
223 | |||
224 | if (statusCode === 404) { | ||
225 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
226 | actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() | ||
227 | return { actor: undefined, refreshed: false } | ||
228 | } | ||
229 | |||
230 | if (result === undefined) { | ||
231 | logger.warn('Cannot fetch remote actor in refresh actor.') | ||
232 | return { actor, refreshed: false } | ||
233 | } | ||
234 | |||
235 | return sequelizeTypescript.transaction(async t => { | ||
236 | updateInstanceWithAnother(actor, result.actor) | ||
237 | |||
238 | if (result.avatarName !== undefined) { | ||
239 | await updateActorAvatarInstance(actor, result.avatarName, t) | ||
240 | } | ||
241 | |||
242 | // Force update | ||
243 | actor.setDataValue('updatedAt', new Date()) | ||
244 | await actor.save({ transaction: t }) | ||
245 | |||
246 | if (actor.Account) { | ||
247 | actor.Account.set('name', result.name) | ||
248 | actor.Account.set('description', result.summary) | ||
249 | |||
250 | await actor.Account.save({ transaction: t }) | ||
251 | } else if (actor.VideoChannel) { | ||
252 | actor.VideoChannel.set('name', result.name) | ||
253 | actor.VideoChannel.set('description', result.summary) | ||
254 | actor.VideoChannel.set('support', result.support) | ||
255 | |||
256 | await actor.VideoChannel.save({ transaction: t }) | ||
257 | } | ||
258 | |||
259 | return { refreshed: true, actor } | ||
260 | }) | ||
261 | } catch (err) { | ||
262 | logger.warn('Cannot refresh actor.', { err }) | ||
263 | return { actor, refreshed: false } | ||
264 | } | ||
265 | } | ||
266 | |||
206 | export { | 267 | export { |
207 | getOrCreateActorAndServerAndModel, | 268 | getOrCreateActorAndServerAndModel, |
208 | buildActorInstance, | 269 | buildActorInstance, |
@@ -210,6 +271,7 @@ export { | |||
210 | fetchActorTotalItems, | 271 | fetchActorTotalItems, |
211 | fetchAvatarIfExists, | 272 | fetchAvatarIfExists, |
212 | updateActorInstance, | 273 | updateActorInstance, |
274 | refreshActorIfNeeded, | ||
213 | updateActorAvatarInstance, | 275 | updateActorAvatarInstance, |
214 | addFetchOutboxJob | 276 | addFetchOutboxJob |
215 | } | 277 | } |
@@ -293,16 +355,19 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe | |||
293 | 355 | ||
294 | logger.info('Fetching remote actor %s.', actorUrl) | 356 | logger.info('Fetching remote actor %s.', actorUrl) |
295 | 357 | ||
296 | const requestResult = await doRequest(options) | 358 | const requestResult = await doRequest<ActivityPubActor>(options) |
297 | normalizeActor(requestResult.body) | 359 | normalizeActor(requestResult.body) |
298 | 360 | ||
299 | const actorJSON: ActivityPubActor = requestResult.body | 361 | const actorJSON = requestResult.body |
300 | |||
301 | if (isActorObjectValid(actorJSON) === false) { | 362 | if (isActorObjectValid(actorJSON) === false) { |
302 | logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON }) | 363 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) |
303 | return { result: undefined, statusCode: requestResult.response.statusCode } | 364 | return { result: undefined, statusCode: requestResult.response.statusCode } |
304 | } | 365 | } |
305 | 366 | ||
367 | if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { | ||
368 | throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id) | ||
369 | } | ||
370 | |||
306 | const followersCount = await fetchActorTotalItems(actorJSON.followers) | 371 | const followersCount = await fetchActorTotalItems(actorJSON.followers) |
307 | const followingCount = await fetchActorTotalItems(actorJSON.following) | 372 | const followingCount = await fetchActorTotalItems(actorJSON.following) |
308 | 373 | ||
@@ -371,59 +436,3 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu | |||
371 | 436 | ||
372 | return videoChannelCreated | 437 | return videoChannelCreated |
373 | } | 438 | } |
374 | |||
375 | async function refreshActorIfNeeded ( | ||
376 | actorArg: ActorModel, | ||
377 | fetchedType: ActorFetchByUrlType | ||
378 | ): Promise<{ actor: ActorModel, refreshed: boolean }> { | ||
379 | if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false } | ||
380 | |||
381 | // We need more attributes | ||
382 | const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url) | ||
383 | |||
384 | try { | ||
385 | const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) | ||
386 | const { result, statusCode } = await fetchRemoteActor(actorUrl) | ||
387 | |||
388 | if (statusCode === 404) { | ||
389 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
390 | actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() | ||
391 | return { actor: undefined, refreshed: false } | ||
392 | } | ||
393 | |||
394 | if (result === undefined) { | ||
395 | logger.warn('Cannot fetch remote actor in refresh actor.') | ||
396 | return { actor, refreshed: false } | ||
397 | } | ||
398 | |||
399 | return sequelizeTypescript.transaction(async t => { | ||
400 | updateInstanceWithAnother(actor, result.actor) | ||
401 | |||
402 | if (result.avatarName !== undefined) { | ||
403 | await updateActorAvatarInstance(actor, result.avatarName, t) | ||
404 | } | ||
405 | |||
406 | // Force update | ||
407 | actor.setDataValue('updatedAt', new Date()) | ||
408 | await actor.save({ transaction: t }) | ||
409 | |||
410 | if (actor.Account) { | ||
411 | actor.Account.set('name', result.name) | ||
412 | actor.Account.set('description', result.summary) | ||
413 | |||
414 | await actor.Account.save({ transaction: t }) | ||
415 | } else if (actor.VideoChannel) { | ||
416 | actor.VideoChannel.set('name', result.name) | ||
417 | actor.VideoChannel.set('description', result.summary) | ||
418 | actor.VideoChannel.set('support', result.support) | ||
419 | |||
420 | await actor.VideoChannel.save({ transaction: t }) | ||
421 | } | ||
422 | |||
423 | return { refreshed: true, actor } | ||
424 | }) | ||
425 | } catch (err) { | ||
426 | logger.warn('Cannot refresh actor.', { err }) | ||
427 | return { actor, refreshed: false } | ||
428 | } | ||
429 | } | ||
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts index f6f068b45..9a40414bb 100644 --- a/server/lib/activitypub/cache-file.ts +++ b/server/lib/activitypub/cache-file.ts | |||
@@ -1,11 +1,28 @@ | |||
1 | import { CacheFileObject } from '../../../shared/index' | 1 | import { ActivityPlaylistUrlObject, ActivityVideoUrlObject, CacheFileObject } from '../../../shared/index' |
2 | import { VideoModel } from '../../models/video/video' | 2 | import { VideoModel } from '../../models/video/video' |
3 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 3 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
4 | import { Transaction } from 'sequelize' | 4 | import { Transaction } from 'sequelize' |
5 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
5 | 6 | ||
6 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { | 7 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { |
7 | const url = cacheFileObject.url | ||
8 | 8 | ||
9 | if (cacheFileObject.url.mediaType === 'application/x-mpegURL') { | ||
10 | const url = cacheFileObject.url | ||
11 | |||
12 | const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS) | ||
13 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) | ||
14 | |||
15 | return { | ||
16 | expiresOn: new Date(cacheFileObject.expires), | ||
17 | url: cacheFileObject.id, | ||
18 | fileUrl: url.href, | ||
19 | strategy: null, | ||
20 | videoStreamingPlaylistId: playlist.id, | ||
21 | actorId: byActor.id | ||
22 | } | ||
23 | } | ||
24 | |||
25 | const url = cacheFileObject.url | ||
9 | const videoFile = video.VideoFiles.find(f => { | 26 | const videoFile = video.VideoFiles.find(f => { |
10 | return f.resolution === url.height && f.fps === url.fps | 27 | return f.resolution === url.height && f.fps === url.fps |
11 | }) | 28 | }) |
@@ -15,7 +32,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject | |||
15 | return { | 32 | return { |
16 | expiresOn: new Date(cacheFileObject.expires), | 33 | expiresOn: new Date(cacheFileObject.expires), |
17 | url: cacheFileObject.id, | 34 | url: cacheFileObject.id, |
18 | fileUrl: cacheFileObject.url.href, | 35 | fileUrl: url.href, |
19 | strategy: null, | 36 | strategy: null, |
20 | videoFileId: videoFile.id, | 37 | videoFileId: videoFile.id, |
21 | actorId: byActor.id | 38 | actorId: byActor.id |
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts index 55912341c..1b9b14c2e 100644 --- a/server/lib/activitypub/crawl.ts +++ b/server/lib/activitypub/crawl.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' | 1 | import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' |
2 | import { doRequest } from '../../helpers/requests' | 2 | import { doRequest } from '../../helpers/requests' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import Bluebird = require('bluebird') | 4 | import * as Bluebird from 'bluebird' |
5 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | ||
5 | 6 | ||
6 | async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { | 7 | async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { |
7 | logger.info('Crawling ActivityPub data on %s.', uri) | 8 | logger.info('Crawling ActivityPub data on %s.', uri) |
@@ -14,7 +15,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr | |||
14 | timeout: JOB_REQUEST_TIMEOUT | 15 | timeout: JOB_REQUEST_TIMEOUT |
15 | } | 16 | } |
16 | 17 | ||
17 | const response = await doRequest(options) | 18 | const response = await doRequest<ActivityPubOrderedCollection<T>>(options) |
18 | const firstBody = response.body | 19 | const firstBody = response.body |
19 | 20 | ||
20 | let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT | 21 | let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT |
@@ -23,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr | |||
23 | while (nextLink && i < limit) { | 24 | while (nextLink && i < limit) { |
24 | options.uri = nextLink | 25 | options.uri = nextLink |
25 | 26 | ||
26 | const { body } = await doRequest(options) | 27 | const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options) |
27 | nextLink = body.next | 28 | nextLink = body.next |
28 | i++ | 29 | i++ |
29 | 30 | ||
diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts index db4980a72..5466739c1 100644 --- a/server/lib/activitypub/process/index.ts +++ b/server/lib/activitypub/process/index.ts | |||
@@ -1,9 +1 @@ | |||
1 | export * from './process' | export * from './process' | |
2 | export * from './process-accept' | ||
3 | export * from './process-announce' | ||
4 | export * from './process-create' | ||
5 | export * from './process-delete' | ||
6 | export * from './process-follow' | ||
7 | export * from './process-like' | ||
8 | export * from './process-undo' | ||
9 | export * from './process-update' | ||
diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts index 89bda9c32..ebb275e34 100644 --- a/server/lib/activitypub/process/process-accept.ts +++ b/server/lib/activitypub/process/process-accept.ts | |||
@@ -24,6 +24,7 @@ async function processAccept (actor: ActorModel, targetActor: ActorModel) { | |||
24 | if (follow.state !== 'accepted') { | 24 | if (follow.state !== 'accepted') { |
25 | follow.set('state', 'accepted') | 25 | follow.set('state', 'accepted') |
26 | await follow.save() | 26 | await follow.save() |
27 | |||
27 | await addFetchOutboxJob(targetActor) | 28 | await addFetchOutboxJob(targetActor) |
28 | } | 29 | } |
29 | } | 30 | } |
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts index cc88b5423..23310b41e 100644 --- a/server/lib/activitypub/process/process-announce.ts +++ b/server/lib/activitypub/process/process-announce.ts | |||
@@ -5,6 +5,8 @@ import { ActorModel } from '../../../models/activitypub/actor' | |||
5 | import { VideoShareModel } from '../../../models/video/video-share' | 5 | import { VideoShareModel } from '../../../models/video/video-share' |
6 | import { forwardVideoRelatedActivity } from '../send/utils' | 6 | import { forwardVideoRelatedActivity } from '../send/utils' |
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
8 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
9 | import { Notifier } from '../../notifier' | ||
8 | 10 | ||
9 | async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) { | 11 | async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) { |
10 | return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity) | 12 | return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity) |
@@ -21,9 +23,9 @@ export { | |||
21 | async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) { | 23 | async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) { |
22 | const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id | 24 | const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id |
23 | 25 | ||
24 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri }) | 26 | const { video, created: videoCreated } = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri }) |
25 | 27 | ||
26 | return sequelizeTypescript.transaction(async t => { | 28 | await sequelizeTypescript.transaction(async t => { |
27 | // Add share entry | 29 | // Add share entry |
28 | 30 | ||
29 | const share = { | 31 | const share = { |
@@ -49,4 +51,6 @@ async function processVideoShare (actorAnnouncer: ActorModel, activity: Activity | |||
49 | 51 | ||
50 | return undefined | 52 | return undefined |
51 | }) | 53 | }) |
54 | |||
55 | if (videoCreated) Notifier.Instance.notifyOnNewVideo(video) | ||
52 | } | 56 | } |
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index cefe89db0..5f4d793a5 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,34 +1,44 @@ | |||
1 | import { ActivityCreate, CacheFileObject, VideoAbuseState, VideoTorrentObject } from '../../../../shared' | 1 | import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared' |
2 | import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' | 2 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' |
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
5 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
6 | import { sequelizeTypescript } from '../../../initializers' | 5 | import { sequelizeTypescript } from '../../../initializers' |
7 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
8 | import { ActorModel } from '../../../models/activitypub/actor' | 6 | import { ActorModel } from '../../../models/activitypub/actor' |
9 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
10 | import { addVideoComment, resolveThread } from '../video-comments' | 7 | import { addVideoComment, resolveThread } from '../video-comments' |
11 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 8 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
12 | import { forwardVideoRelatedActivity } from '../send/utils' | 9 | import { forwardVideoRelatedActivity } from '../send/utils' |
13 | import { Redis } from '../../redis' | ||
14 | import { createOrUpdateCacheFile } from '../cache-file' | 10 | import { createOrUpdateCacheFile } from '../cache-file' |
11 | import { Notifier } from '../../notifier' | ||
12 | import { processViewActivity } from './process-view' | ||
13 | import { processDislikeActivity } from './process-dislike' | ||
14 | import { processFlagActivity } from './process-flag' | ||
15 | 15 | ||
16 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { | 16 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { |
17 | const activityObject = activity.object | 17 | const activityObject = activity.object |
18 | const activityType = activityObject.type | 18 | const activityType = activityObject.type |
19 | 19 | ||
20 | if (activityType === 'View') { | 20 | if (activityType === 'View') { |
21 | return processCreateView(byActor, activity) | 21 | return processViewActivity(activity, byActor) |
22 | } else if (activityType === 'Dislike') { | 22 | } |
23 | return retryTransactionWrapper(processCreateDislike, byActor, activity) | 23 | |
24 | } else if (activityType === 'Video') { | 24 | if (activityType === 'Dislike') { |
25 | return retryTransactionWrapper(processDislikeActivity, activity, byActor) | ||
26 | } | ||
27 | |||
28 | if (activityType === 'Flag') { | ||
29 | return retryTransactionWrapper(processFlagActivity, activity, byActor) | ||
30 | } | ||
31 | |||
32 | if (activityType === 'Video') { | ||
25 | return processCreateVideo(activity) | 33 | return processCreateVideo(activity) |
26 | } else if (activityType === 'Flag') { | 34 | } |
27 | return retryTransactionWrapper(processCreateVideoAbuse, byActor, activityObject as VideoAbuseObject) | 35 | |
28 | } else if (activityType === 'Note') { | 36 | if (activityType === 'Note') { |
29 | return retryTransactionWrapper(processCreateVideoComment, byActor, activity) | 37 | return retryTransactionWrapper(processCreateVideoComment, activity, byActor) |
30 | } else if (activityType === 'CacheFile') { | 38 | } |
31 | return retryTransactionWrapper(processCacheFile, byActor, activity) | 39 | |
40 | if (activityType === 'CacheFile') { | ||
41 | return retryTransactionWrapper(processCacheFile, activity, byActor) | ||
32 | } | 42 | } |
33 | 43 | ||
34 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) | 44 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) |
@@ -46,60 +56,14 @@ export { | |||
46 | async function processCreateVideo (activity: ActivityCreate) { | 56 | async function processCreateVideo (activity: ActivityCreate) { |
47 | const videoToCreateData = activity.object as VideoTorrentObject | 57 | const videoToCreateData = activity.object as VideoTorrentObject |
48 | 58 | ||
49 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData }) | 59 | const { video, created } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData }) |
50 | |||
51 | return video | ||
52 | } | ||
53 | |||
54 | async function processCreateDislike (byActor: ActorModel, activity: ActivityCreate) { | ||
55 | const dislike = activity.object as DislikeObject | ||
56 | const byAccount = byActor.Account | ||
57 | |||
58 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
59 | |||
60 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) | ||
61 | |||
62 | return sequelizeTypescript.transaction(async t => { | ||
63 | const rate = { | ||
64 | type: 'dislike' as 'dislike', | ||
65 | videoId: video.id, | ||
66 | accountId: byAccount.id | ||
67 | } | ||
68 | const [ , created ] = await AccountVideoRateModel.findOrCreate({ | ||
69 | where: rate, | ||
70 | defaults: rate, | ||
71 | transaction: t | ||
72 | }) | ||
73 | if (created === true) await video.increment('dislikes', { transaction: t }) | ||
74 | |||
75 | if (video.isOwned() && created === true) { | ||
76 | // Don't resend the activity to the sender | ||
77 | const exceptions = [ byActor ] | ||
78 | |||
79 | await forwardVideoRelatedActivity(activity, t, exceptions, video) | ||
80 | } | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { | ||
85 | const view = activity.object as ViewObject | ||
86 | |||
87 | const options = { | ||
88 | videoObject: view.object, | ||
89 | fetchType: 'only-video' as 'only-video' | ||
90 | } | ||
91 | const { video } = await getOrCreateVideoAndAccountAndChannel(options) | ||
92 | 60 | ||
93 | await Redis.Instance.addVideoView(video.id) | 61 | if (created) Notifier.Instance.notifyOnNewVideo(video) |
94 | 62 | ||
95 | if (video.isOwned()) { | 63 | return video |
96 | // Don't resend the activity to the sender | ||
97 | const exceptions = [ byActor ] | ||
98 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | ||
99 | } | ||
100 | } | 64 | } |
101 | 65 | ||
102 | async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { | 66 | async function processCacheFile (activity: ActivityCreate, byActor: ActorModel) { |
103 | const cacheFile = activity.object as CacheFileObject | 67 | const cacheFile = activity.object as CacheFileObject |
104 | 68 | ||
105 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) | 69 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) |
@@ -115,29 +79,7 @@ async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) | |||
115 | } | 79 | } |
116 | } | 80 | } |
117 | 81 | ||
118 | async function processCreateVideoAbuse (byActor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) { | 82 | async function processCreateVideoComment (activity: ActivityCreate, byActor: ActorModel) { |
119 | logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) | ||
120 | |||
121 | const account = byActor.Account | ||
122 | if (!account) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
123 | |||
124 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoAbuseToCreateData.object }) | ||
125 | |||
126 | return sequelizeTypescript.transaction(async t => { | ||
127 | const videoAbuseData = { | ||
128 | reporterAccountId: account.id, | ||
129 | reason: videoAbuseToCreateData.content, | ||
130 | videoId: video.id, | ||
131 | state: VideoAbuseState.PENDING | ||
132 | } | ||
133 | |||
134 | await VideoAbuseModel.create(videoAbuseData, { transaction: t }) | ||
135 | |||
136 | logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) | ||
137 | }) | ||
138 | } | ||
139 | |||
140 | async function processCreateVideoComment (byActor: ActorModel, activity: ActivityCreate) { | ||
141 | const commentObject = activity.object as VideoCommentObject | 83 | const commentObject = activity.object as VideoCommentObject |
142 | const byAccount = byActor.Account | 84 | const byAccount = byActor.Account |
143 | 85 | ||
@@ -145,7 +87,7 @@ async function processCreateVideoComment (byActor: ActorModel, activity: Activit | |||
145 | 87 | ||
146 | const { video } = await resolveThread(commentObject.inReplyTo) | 88 | const { video } = await resolveThread(commentObject.inReplyTo) |
147 | 89 | ||
148 | const { created } = await addVideoComment(video, commentObject.id) | 90 | const { comment, created } = await addVideoComment(video, commentObject.id) |
149 | 91 | ||
150 | if (video.isOwned() && created === true) { | 92 | if (video.isOwned() && created === true) { |
151 | // Don't resend the activity to the sender | 93 | // Don't resend the activity to the sender |
@@ -153,4 +95,6 @@ async function processCreateVideoComment (byActor: ActorModel, activity: Activit | |||
153 | 95 | ||
154 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | 96 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) |
155 | } | 97 | } |
98 | |||
99 | if (created === true) Notifier.Instance.notifyOnNewComment(comment) | ||
156 | } | 100 | } |
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts new file mode 100644 index 000000000..bfd69e07a --- /dev/null +++ b/server/lib/activitypub/process/process-dislike.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | import { ActivityCreate, ActivityDislike } from '../../../../shared' | ||
2 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { sequelizeTypescript } from '../../../initializers' | ||
5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
6 | import { ActorModel } from '../../../models/activitypub/actor' | ||
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
8 | import { forwardVideoRelatedActivity } from '../send/utils' | ||
9 | import { getVideoDislikeActivityPubUrl } from '../url' | ||
10 | |||
11 | async function processDislikeActivity (activity: ActivityCreate | ActivityDislike, byActor: ActorModel) { | ||
12 | return retryTransactionWrapper(processDislike, activity, byActor) | ||
13 | } | ||
14 | |||
15 | // --------------------------------------------------------------------------- | ||
16 | |||
17 | export { | ||
18 | processDislikeActivity | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: ActorModel) { | ||
24 | const dislikeObject = activity.type === 'Dislike' ? activity.object : (activity.object as DislikeObject).object | ||
25 | const byAccount = byActor.Account | ||
26 | |||
27 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
28 | |||
29 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislikeObject }) | ||
30 | |||
31 | return sequelizeTypescript.transaction(async t => { | ||
32 | const rate = { | ||
33 | type: 'dislike' as 'dislike', | ||
34 | videoId: video.id, | ||
35 | accountId: byAccount.id | ||
36 | } | ||
37 | |||
38 | const [ , created ] = await AccountVideoRateModel.findOrCreate({ | ||
39 | where: rate, | ||
40 | defaults: Object.assign({}, rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }), | ||
41 | transaction: t | ||
42 | }) | ||
43 | if (created === true) await video.increment('dislikes', { transaction: t }) | ||
44 | |||
45 | if (video.isOwned() && created === true) { | ||
46 | // Don't resend the activity to the sender | ||
47 | const exceptions = [ byActor ] | ||
48 | |||
49 | await forwardVideoRelatedActivity(activity, t, exceptions, video) | ||
50 | } | ||
51 | }) | ||
52 | } | ||
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts new file mode 100644 index 000000000..79ce6fb41 --- /dev/null +++ b/server/lib/activitypub/process/process-flag.ts | |||
@@ -0,0 +1,49 @@ | |||
1 | import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../shared' | ||
2 | import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { logger } from '../../../helpers/logger' | ||
5 | import { sequelizeTypescript } from '../../../initializers' | ||
6 | import { ActorModel } from '../../../models/activitypub/actor' | ||
7 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
8 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
9 | import { Notifier } from '../../notifier' | ||
10 | import { getAPId } from '../../../helpers/activitypub' | ||
11 | |||
12 | async function processFlagActivity (activity: ActivityCreate | ActivityFlag, byActor: ActorModel) { | ||
13 | return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor) | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | processFlagActivity | ||
20 | } | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: ActorModel) { | ||
25 | const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject) | ||
26 | |||
27 | logger.debug('Reporting remote abuse for video %s.', getAPId(flag.object)) | ||
28 | |||
29 | const account = byActor.Account | ||
30 | if (!account) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
31 | |||
32 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: flag.object }) | ||
33 | |||
34 | return sequelizeTypescript.transaction(async t => { | ||
35 | const videoAbuseData = { | ||
36 | reporterAccountId: account.id, | ||
37 | reason: flag.content, | ||
38 | videoId: video.id, | ||
39 | state: VideoAbuseState.PENDING | ||
40 | } | ||
41 | |||
42 | const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) | ||
43 | videoAbuseInstance.Video = video | ||
44 | |||
45 | Notifier.Instance.notifyOnNewVideoAbuse(videoAbuseInstance) | ||
46 | |||
47 | logger.info('Remote abuse for video uuid %s created', flag.object) | ||
48 | }) | ||
49 | } | ||
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index 24c9085f7..0cd537187 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts | |||
@@ -5,9 +5,11 @@ import { sequelizeTypescript } from '../../../initializers' | |||
5 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/activitypub/actor' |
6 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 6 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
7 | import { sendAccept } from '../send' | 7 | import { sendAccept } from '../send' |
8 | import { Notifier } from '../../notifier' | ||
9 | import { getAPId } from '../../../helpers/activitypub' | ||
8 | 10 | ||
9 | async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) { | 11 | async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) { |
10 | const activityObject = activity.object | 12 | const activityObject = getAPId(activity.object) |
11 | 13 | ||
12 | return retryTransactionWrapper(processFollow, byActor, activityObject) | 14 | return retryTransactionWrapper(processFollow, byActor, activityObject) |
13 | } | 15 | } |
@@ -21,13 +23,13 @@ export { | |||
21 | // --------------------------------------------------------------------------- | 23 | // --------------------------------------------------------------------------- |
22 | 24 | ||
23 | async function processFollow (actor: ActorModel, targetActorURL: string) { | 25 | async function processFollow (actor: ActorModel, targetActorURL: string) { |
24 | await sequelizeTypescript.transaction(async t => { | 26 | const { actorFollow, created } = await sequelizeTypescript.transaction(async t => { |
25 | const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) | 27 | const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) |
26 | 28 | ||
27 | if (!targetActor) throw new Error('Unknown actor') | 29 | if (!targetActor) throw new Error('Unknown actor') |
28 | if (targetActor.isOwned() === false) throw new Error('This is not a local actor.') | 30 | if (targetActor.isOwned() === false) throw new Error('This is not a local actor.') |
29 | 31 | ||
30 | const [ actorFollow ] = await ActorFollowModel.findOrCreate({ | 32 | const [ actorFollow, created ] = await ActorFollowModel.findOrCreate({ |
31 | where: { | 33 | where: { |
32 | actorId: actor.id, | 34 | actorId: actor.id, |
33 | targetActorId: targetActor.id | 35 | targetActorId: targetActor.id |
@@ -52,8 +54,12 @@ async function processFollow (actor: ActorModel, targetActorURL: string) { | |||
52 | actorFollow.ActorFollowing = targetActor | 54 | actorFollow.ActorFollowing = targetActor |
53 | 55 | ||
54 | // Target sends to actor he accepted the follow request | 56 | // Target sends to actor he accepted the follow request |
55 | return sendAccept(actorFollow) | 57 | await sendAccept(actorFollow) |
58 | |||
59 | return { actorFollow, created } | ||
56 | }) | 60 | }) |
57 | 61 | ||
62 | if (created) Notifier.Instance.notifyOfNewFollow(actorFollow) | ||
63 | |||
58 | logger.info('Actor %s is followed by actor %s.', targetActorURL, actor.url) | 64 | logger.info('Actor %s is followed by actor %s.', targetActorURL, actor.url) |
59 | } | 65 | } |
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts index f7200db61..2a04167d7 100644 --- a/server/lib/activitypub/process/process-like.ts +++ b/server/lib/activitypub/process/process-like.ts | |||
@@ -5,6 +5,8 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat | |||
5 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/activitypub/actor' |
6 | import { forwardVideoRelatedActivity } from '../send/utils' | 6 | import { forwardVideoRelatedActivity } from '../send/utils' |
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
8 | import { getVideoLikeActivityPubUrl } from '../url' | ||
9 | import { getAPId } from '../../../helpers/activitypub' | ||
8 | 10 | ||
9 | async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { | 11 | async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { |
10 | return retryTransactionWrapper(processLikeVideo, byActor, activity) | 12 | return retryTransactionWrapper(processLikeVideo, byActor, activity) |
@@ -19,7 +21,7 @@ export { | |||
19 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
20 | 22 | ||
21 | async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { | 23 | async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { |
22 | const videoUrl = activity.object | 24 | const videoUrl = getAPId(activity.object) |
23 | 25 | ||
24 | const byAccount = byActor.Account | 26 | const byAccount = byActor.Account |
25 | if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) | 27 | if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) |
@@ -34,7 +36,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { | |||
34 | } | 36 | } |
35 | const [ , created ] = await AccountVideoRateModel.findOrCreate({ | 37 | const [ , created ] = await AccountVideoRateModel.findOrCreate({ |
36 | where: rate, | 38 | where: rate, |
37 | defaults: rate, | 39 | defaults: Object.assign({}, rate, { url: getVideoLikeActivityPubUrl(byActor, video) }), |
38 | transaction: t | 40 | transaction: t |
39 | }) | 41 | }) |
40 | if (created === true) await video.increment('likes', { transaction: t }) | 42 | if (created === true) await video.increment('likes', { transaction: t }) |
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index ff019cd8c..ed0177a67 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts | |||
@@ -26,6 +26,10 @@ async function processUndoActivity (activity: ActivityUndo, byActor: ActorModel) | |||
26 | } | 26 | } |
27 | } | 27 | } |
28 | 28 | ||
29 | if (activityToUndo.type === 'Dislike') { | ||
30 | return retryTransactionWrapper(processUndoDislike, byActor, activity) | ||
31 | } | ||
32 | |||
29 | if (activityToUndo.type === 'Follow') { | 33 | if (activityToUndo.type === 'Follow') { |
30 | return retryTransactionWrapper(processUndoFollow, byActor, activityToUndo) | 34 | return retryTransactionWrapper(processUndoFollow, byActor, activityToUndo) |
31 | } | 35 | } |
@@ -55,7 +59,8 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) { | |||
55 | return sequelizeTypescript.transaction(async t => { | 59 | return sequelizeTypescript.transaction(async t => { |
56 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) | 60 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) |
57 | 61 | ||
58 | const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) | 62 | let rate = await AccountVideoRateModel.loadByUrl(likeActivity.id, t) |
63 | if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) | ||
59 | if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) | 64 | if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) |
60 | 65 | ||
61 | await rate.destroy({ transaction: t }) | 66 | await rate.destroy({ transaction: t }) |
@@ -71,14 +76,17 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) { | |||
71 | } | 76 | } |
72 | 77 | ||
73 | async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { | 78 | async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { |
74 | const dislike = activity.object.object as DislikeObject | 79 | const dislike = activity.object.type === 'Dislike' |
80 | ? activity.object | ||
81 | : activity.object.object as DislikeObject | ||
75 | 82 | ||
76 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) | 83 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) |
77 | 84 | ||
78 | return sequelizeTypescript.transaction(async t => { | 85 | return sequelizeTypescript.transaction(async t => { |
79 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) | 86 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) |
80 | 87 | ||
81 | const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) | 88 | let rate = await AccountVideoRateModel.loadByUrl(dislike.id, t) |
89 | if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) | ||
82 | if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) | 90 | if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) |
83 | 91 | ||
84 | await rate.destroy({ transaction: t }) | 92 | await rate.destroy({ transaction: t }) |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index bd4013555..c6b42d846 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -51,7 +51,7 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) | |||
51 | return undefined | 51 | return undefined |
52 | } | 52 | } |
53 | 53 | ||
54 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id }) | 54 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id, allowRefresh: false }) |
55 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) | 55 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) |
56 | 56 | ||
57 | const updateOptions = { | 57 | const updateOptions = { |
@@ -59,7 +59,6 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) | |||
59 | videoObject, | 59 | videoObject, |
60 | account: actor.Account, | 60 | account: actor.Account, |
61 | channel: channelActor.VideoChannel, | 61 | channel: channelActor.VideoChannel, |
62 | updateViews: true, | ||
63 | overrideTo: activity.to | 62 | overrideTo: activity.to |
64 | } | 63 | } |
65 | return updateVideoFromAP(updateOptions) | 64 | return updateVideoFromAP(updateOptions) |
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts new file mode 100644 index 000000000..8f66d3630 --- /dev/null +++ b/server/lib/activitypub/process/process-view.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import { ActorModel } from '../../../models/activitypub/actor' | ||
2 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
3 | import { forwardVideoRelatedActivity } from '../send/utils' | ||
4 | import { Redis } from '../../redis' | ||
5 | import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub' | ||
6 | |||
7 | async function processViewActivity (activity: ActivityView | ActivityCreate, byActor: ActorModel) { | ||
8 | return processCreateView(activity, byActor) | ||
9 | } | ||
10 | |||
11 | // --------------------------------------------------------------------------- | ||
12 | |||
13 | export { | ||
14 | processViewActivity | ||
15 | } | ||
16 | |||
17 | // --------------------------------------------------------------------------- | ||
18 | |||
19 | async function processCreateView (activity: ActivityView | ActivityCreate, byActor: ActorModel) { | ||
20 | const videoObject = activity.type === 'View' ? activity.object : (activity.object as ViewObject).object | ||
21 | |||
22 | const options = { | ||
23 | videoObject: videoObject, | ||
24 | fetchType: 'only-video' as 'only-video' | ||
25 | } | ||
26 | const { video } = await getOrCreateVideoAndAccountAndChannel(options) | ||
27 | |||
28 | await Redis.Instance.addVideoView(video.id) | ||
29 | |||
30 | if (video.isOwned()) { | ||
31 | // Don't resend the activity to the sender | ||
32 | const exceptions = [ byActor ] | ||
33 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | ||
34 | } | ||
35 | } | ||
diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts index b263f1ea2..9dd241402 100644 --- a/server/lib/activitypub/process/process.ts +++ b/server/lib/activitypub/process/process.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 1 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
2 | import { getActorUrl } from '../../../helpers/activitypub' | 2 | import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { ActorModel } from '../../../models/activitypub/actor' | 4 | import { ActorModel } from '../../../models/activitypub/actor' |
5 | import { processAcceptActivity } from './process-accept' | 5 | import { processAcceptActivity } from './process-accept' |
@@ -12,6 +12,9 @@ import { processRejectActivity } from './process-reject' | |||
12 | import { processUndoActivity } from './process-undo' | 12 | import { processUndoActivity } from './process-undo' |
13 | import { processUpdateActivity } from './process-update' | 13 | import { processUpdateActivity } from './process-update' |
14 | import { getOrCreateActorAndServerAndModel } from '../actor' | 14 | import { getOrCreateActorAndServerAndModel } from '../actor' |
15 | import { processDislikeActivity } from './process-dislike' | ||
16 | import { processFlagActivity } from './process-flag' | ||
17 | import { processViewActivity } from './process-view' | ||
15 | 18 | ||
16 | const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: ActorModel, inboxActor?: ActorModel) => Promise<any> } = { | 19 | const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: ActorModel, inboxActor?: ActorModel) => Promise<any> } = { |
17 | Create: processCreateActivity, | 20 | Create: processCreateActivity, |
@@ -22,27 +25,41 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac | |||
22 | Reject: processRejectActivity, | 25 | Reject: processRejectActivity, |
23 | Announce: processAnnounceActivity, | 26 | Announce: processAnnounceActivity, |
24 | Undo: processUndoActivity, | 27 | Undo: processUndoActivity, |
25 | Like: processLikeActivity | 28 | Like: processLikeActivity, |
29 | Dislike: processDislikeActivity, | ||
30 | Flag: processFlagActivity, | ||
31 | View: processViewActivity | ||
26 | } | 32 | } |
27 | 33 | ||
28 | async function processActivities (activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel) { | 34 | async function processActivities ( |
35 | activities: Activity[], | ||
36 | options: { | ||
37 | signatureActor?: ActorModel | ||
38 | inboxActor?: ActorModel | ||
39 | outboxUrl?: string | ||
40 | } = {}) { | ||
29 | const actorsCache: { [ url: string ]: ActorModel } = {} | 41 | const actorsCache: { [ url: string ]: ActorModel } = {} |
30 | 42 | ||
31 | for (const activity of activities) { | 43 | for (const activity of activities) { |
32 | if (!signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) { | 44 | if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].includes(activity.type) === false) { |
33 | logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type) | 45 | logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type) |
34 | continue | 46 | continue |
35 | } | 47 | } |
36 | 48 | ||
37 | const actorUrl = getActorUrl(activity.actor) | 49 | const actorUrl = getAPId(activity.actor) |
38 | 50 | ||
39 | // When we fetch remote data, we don't have signature | 51 | // When we fetch remote data, we don't have signature |
40 | if (signatureActor && actorUrl !== signatureActor.url) { | 52 | if (options.signatureActor && actorUrl !== options.signatureActor.url) { |
41 | logger.warn('Signature mismatch between %s and %s.', actorUrl, signatureActor.url) | 53 | logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, options.signatureActor.url) |
42 | continue | 54 | continue |
43 | } | 55 | } |
44 | 56 | ||
45 | const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl) | 57 | if (options.outboxUrl && checkUrlsSameHost(options.outboxUrl, actorUrl) !== true) { |
58 | logger.warn('Host mismatch between outbox URL %s and actor URL %s, skipping.', options.outboxUrl, actorUrl) | ||
59 | continue | ||
60 | } | ||
61 | |||
62 | const byActor = options.signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl) | ||
46 | actorsCache[actorUrl] = byActor | 63 | actorsCache[actorUrl] = byActor |
47 | 64 | ||
48 | const activityProcessor = processActivity[activity.type] | 65 | const activityProcessor = processActivity[activity.type] |
@@ -52,7 +69,7 @@ async function processActivities (activities: Activity[], signatureActor?: Actor | |||
52 | } | 69 | } |
53 | 70 | ||
54 | try { | 71 | try { |
55 | await activityProcessor(activity, byActor, inboxActor) | 72 | await activityProcessor(activity, byActor, options.inboxActor) |
56 | } catch (err) { | 73 | } catch (err) { |
57 | logger.warn('Cannot process activity %s.', activity.type, { err }) | 74 | logger.warn('Cannot process activity %s.', activity.type, { err }) |
58 | } | 75 | } |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 285edba3b..ef20e404c 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -3,9 +3,7 @@ import { ActivityAudience, ActivityCreate } from '../../../../shared/models/acti | |||
3 | import { VideoPrivacy } from '../../../../shared/models/videos' | 3 | import { VideoPrivacy } from '../../../../shared/models/videos' |
4 | import { ActorModel } from '../../../models/activitypub/actor' | 4 | import { ActorModel } from '../../../models/activitypub/actor' |
5 | import { VideoModel } from '../../../models/video/video' | 5 | import { VideoModel } from '../../../models/video/video' |
6 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../../models/video/video-comment' |
8 | import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' | ||
9 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 7 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
10 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' | 8 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' |
11 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
@@ -25,31 +23,14 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) { | |||
25 | return broadcastToFollowers(createActivity, byActor, [ byActor ], t) | 23 | return broadcastToFollowers(createActivity, byActor, [ byActor ], t) |
26 | } | 24 | } |
27 | 25 | ||
28 | async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel) { | 26 | async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, fileRedundancy: VideoRedundancyModel) { |
29 | if (!video.VideoChannel.Account.Actor.serverId) return // Local | ||
30 | |||
31 | const url = getVideoAbuseActivityPubUrl(videoAbuse) | ||
32 | |||
33 | logger.info('Creating job to send video abuse %s.', url) | ||
34 | |||
35 | // Custom audience, we only send the abuse to the origin instance | ||
36 | const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } | ||
37 | const createActivity = buildCreateActivity(url, byActor, videoAbuse.toActivityPubObject(), audience) | ||
38 | |||
39 | return unicastTo(createActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) | ||
40 | } | ||
41 | |||
42 | async function sendCreateCacheFile (byActor: ActorModel, fileRedundancy: VideoRedundancyModel) { | ||
43 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url) | 27 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url) |
44 | 28 | ||
45 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(fileRedundancy.VideoFile.Video.id) | ||
46 | const redundancyObject = fileRedundancy.toActivityPubObject() | ||
47 | |||
48 | return sendVideoRelatedCreateActivity({ | 29 | return sendVideoRelatedCreateActivity({ |
49 | byActor, | 30 | byActor, |
50 | video, | 31 | video, |
51 | url: fileRedundancy.url, | 32 | url: fileRedundancy.url, |
52 | object: redundancyObject | 33 | object: fileRedundancy.toActivityPubObject() |
53 | }) | 34 | }) |
54 | } | 35 | } |
55 | 36 | ||
@@ -91,37 +72,6 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio | |||
91 | return unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl) | 72 | return unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl) |
92 | } | 73 | } |
93 | 74 | ||
94 | async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
95 | logger.info('Creating job to send view of %s.', video.url) | ||
96 | |||
97 | const url = getVideoViewActivityPubUrl(byActor, video) | ||
98 | const viewActivity = buildViewActivity(byActor, video) | ||
99 | |||
100 | return sendVideoRelatedCreateActivity({ | ||
101 | // Use the server actor to send the view | ||
102 | byActor, | ||
103 | video, | ||
104 | url, | ||
105 | object: viewActivity, | ||
106 | transaction: t | ||
107 | }) | ||
108 | } | ||
109 | |||
110 | async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
111 | logger.info('Creating job to dislike %s.', video.url) | ||
112 | |||
113 | const url = getVideoDislikeActivityPubUrl(byActor, video) | ||
114 | const dislikeActivity = buildDislikeActivity(byActor, video) | ||
115 | |||
116 | return sendVideoRelatedCreateActivity({ | ||
117 | byActor, | ||
118 | video, | ||
119 | url, | ||
120 | object: dislikeActivity, | ||
121 | transaction: t | ||
122 | }) | ||
123 | } | ||
124 | |||
125 | function buildCreateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate { | 75 | function buildCreateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate { |
126 | if (!audience) audience = getAudience(byActor) | 76 | if (!audience) audience = getAudience(byActor) |
127 | 77 | ||
@@ -136,31 +86,11 @@ function buildCreateActivity (url: string, byActor: ActorModel, object: any, aud | |||
136 | ) | 86 | ) |
137 | } | 87 | } |
138 | 88 | ||
139 | function buildDislikeActivity (byActor: ActorModel, video: VideoModel) { | ||
140 | return { | ||
141 | type: 'Dislike', | ||
142 | actor: byActor.url, | ||
143 | object: video.url | ||
144 | } | ||
145 | } | ||
146 | |||
147 | function buildViewActivity (byActor: ActorModel, video: VideoModel) { | ||
148 | return { | ||
149 | type: 'View', | ||
150 | actor: byActor.url, | ||
151 | object: video.url | ||
152 | } | ||
153 | } | ||
154 | |||
155 | // --------------------------------------------------------------------------- | 89 | // --------------------------------------------------------------------------- |
156 | 90 | ||
157 | export { | 91 | export { |
158 | sendCreateVideo, | 92 | sendCreateVideo, |
159 | sendVideoAbuse, | ||
160 | buildCreateActivity, | 93 | buildCreateActivity, |
161 | sendCreateView, | ||
162 | sendCreateDislike, | ||
163 | buildDislikeActivity, | ||
164 | sendCreateVideoComment, | 94 | sendCreateVideoComment, |
165 | sendCreateCacheFile | 95 | sendCreateCacheFile |
166 | } | 96 | } |
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts new file mode 100644 index 000000000..a88436f2c --- /dev/null +++ b/server/lib/activitypub/send/send-dislike.ts | |||
@@ -0,0 +1,41 @@ | |||
1 | import { Transaction } from 'sequelize' | ||
2 | import { ActorModel } from '../../../models/activitypub/actor' | ||
3 | import { VideoModel } from '../../../models/video/video' | ||
4 | import { getVideoDislikeActivityPubUrl } from '../url' | ||
5 | import { logger } from '../../../helpers/logger' | ||
6 | import { ActivityAudience, ActivityDislike } from '../../../../shared/models/activitypub' | ||
7 | import { sendVideoRelatedActivity } from './utils' | ||
8 | import { audiencify, getAudience } from '../audience' | ||
9 | |||
10 | async function sendDislike (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
11 | logger.info('Creating job to dislike %s.', video.url) | ||
12 | |||
13 | const activityBuilder = (audience: ActivityAudience) => { | ||
14 | const url = getVideoDislikeActivityPubUrl(byActor, video) | ||
15 | |||
16 | return buildDislikeActivity(url, byActor, video, audience) | ||
17 | } | ||
18 | |||
19 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) | ||
20 | } | ||
21 | |||
22 | function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityDislike { | ||
23 | if (!audience) audience = getAudience(byActor) | ||
24 | |||
25 | return audiencify( | ||
26 | { | ||
27 | id: url, | ||
28 | type: 'Dislike' as 'Dislike', | ||
29 | actor: byActor.url, | ||
30 | object: video.url | ||
31 | }, | ||
32 | audience | ||
33 | ) | ||
34 | } | ||
35 | |||
36 | // --------------------------------------------------------------------------- | ||
37 | |||
38 | export { | ||
39 | sendDislike, | ||
40 | buildDislikeActivity | ||
41 | } | ||
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts new file mode 100644 index 000000000..96a7311b9 --- /dev/null +++ b/server/lib/activitypub/send/send-flag.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import { ActorModel } from '../../../models/activitypub/actor' | ||
2 | import { VideoModel } from '../../../models/video/video' | ||
3 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
4 | import { getVideoAbuseActivityPubUrl } from '../url' | ||
5 | import { unicastTo } from './utils' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' | ||
8 | import { audiencify, getAudience } from '../audience' | ||
9 | |||
10 | async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel) { | ||
11 | if (!video.VideoChannel.Account.Actor.serverId) return // Local user | ||
12 | |||
13 | const url = getVideoAbuseActivityPubUrl(videoAbuse) | ||
14 | |||
15 | logger.info('Creating job to send video abuse %s.', url) | ||
16 | |||
17 | // Custom audience, we only send the abuse to the origin instance | ||
18 | const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } | ||
19 | const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience) | ||
20 | |||
21 | return unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) | ||
22 | } | ||
23 | |||
24 | function buildFlagActivity (url: string, byActor: ActorModel, videoAbuse: VideoAbuseModel, audience: ActivityAudience): ActivityFlag { | ||
25 | if (!audience) audience = getAudience(byActor) | ||
26 | |||
27 | const activity = Object.assign( | ||
28 | { id: url, actor: byActor.url }, | ||
29 | videoAbuse.toActivityPubObject() | ||
30 | ) | ||
31 | |||
32 | return audiencify(activity, audience) | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | export { | ||
38 | sendVideoAbuse | ||
39 | } | ||
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts index 89307acc6..35227887a 100644 --- a/server/lib/activitypub/send/send-like.ts +++ b/server/lib/activitypub/send/send-like.ts | |||
@@ -24,8 +24,8 @@ function buildLikeActivity (url: string, byActor: ActorModel, video: VideoModel, | |||
24 | 24 | ||
25 | return audiencify( | 25 | return audiencify( |
26 | { | 26 | { |
27 | type: 'Like' as 'Like', | ||
28 | id: url, | 27 | id: url, |
28 | type: 'Like' as 'Like', | ||
29 | actor: byActor.url, | 29 | actor: byActor.url, |
30 | object: video.url | 30 | object: video.url |
31 | }, | 31 | }, |
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index 5236d2cb3..ecbf605d6 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts | |||
@@ -2,7 +2,7 @@ import { Transaction } from 'sequelize' | |||
2 | import { | 2 | import { |
3 | ActivityAnnounce, | 3 | ActivityAnnounce, |
4 | ActivityAudience, | 4 | ActivityAudience, |
5 | ActivityCreate, | 5 | ActivityCreate, ActivityDislike, |
6 | ActivityFollow, | 6 | ActivityFollow, |
7 | ActivityLike, | 7 | ActivityLike, |
8 | ActivityUndo | 8 | ActivityUndo |
@@ -13,13 +13,14 @@ import { VideoModel } from '../../../models/video/video' | |||
13 | import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' | 13 | import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' |
14 | import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 14 | import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
15 | import { audiencify, getAudience } from '../audience' | 15 | import { audiencify, getAudience } from '../audience' |
16 | import { buildCreateActivity, buildDislikeActivity } from './send-create' | 16 | import { buildCreateActivity } from './send-create' |
17 | import { buildFollowActivity } from './send-follow' | 17 | import { buildFollowActivity } from './send-follow' |
18 | import { buildLikeActivity } from './send-like' | 18 | import { buildLikeActivity } from './send-like' |
19 | import { VideoShareModel } from '../../../models/video/video-share' | 19 | import { VideoShareModel } from '../../../models/video/video-share' |
20 | import { buildAnnounceWithVideoAudience } from './send-announce' | 20 | import { buildAnnounceWithVideoAudience } from './send-announce' |
21 | import { logger } from '../../../helpers/logger' | 21 | import { logger } from '../../../helpers/logger' |
22 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' | 22 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' |
23 | import { buildDislikeActivity } from './send-dislike' | ||
23 | 24 | ||
24 | async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { | 25 | async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { |
25 | const me = actorFollow.ActorFollower | 26 | const me = actorFollow.ActorFollower |
@@ -64,16 +65,16 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans | |||
64 | logger.info('Creating job to undo a dislike of video %s.', video.url) | 65 | logger.info('Creating job to undo a dislike of video %s.', video.url) |
65 | 66 | ||
66 | const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) | 67 | const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) |
67 | const dislikeActivity = buildDislikeActivity(byActor, video) | 68 | const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video) |
68 | const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity) | ||
69 | 69 | ||
70 | return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t }) | 70 | return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t }) |
71 | } | 71 | } |
72 | 72 | ||
73 | async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { | 73 | async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { |
74 | logger.info('Creating job to undo cache file %s.', redundancyModel.url) | 74 | logger.info('Creating job to undo cache file %s.', redundancyModel.url) |
75 | 75 | ||
76 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) | 76 | const videoId = redundancyModel.getVideo().id |
77 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
77 | const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) | 78 | const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) |
78 | 79 | ||
79 | return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) | 80 | return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) |
@@ -94,7 +95,7 @@ export { | |||
94 | function undoActivityData ( | 95 | function undoActivityData ( |
95 | url: string, | 96 | url: string, |
96 | byActor: ActorModel, | 97 | byActor: ActorModel, |
97 | object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, | 98 | object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, |
98 | audience?: ActivityAudience | 99 | audience?: ActivityAudience |
99 | ): ActivityUndo { | 100 | ): ActivityUndo { |
100 | if (!audience) audience = getAudience(byActor) | 101 | if (!audience) audience = getAudience(byActor) |
@@ -114,7 +115,7 @@ async function sendUndoVideoRelatedActivity (options: { | |||
114 | byActor: ActorModel, | 115 | byActor: ActorModel, |
115 | video: VideoModel, | 116 | video: VideoModel, |
116 | url: string, | 117 | url: string, |
117 | activity: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, | 118 | activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, |
118 | transaction: Transaction | 119 | transaction: Transaction |
119 | }) { | 120 | }) { |
120 | const activityBuilder = (audience: ActivityAudience) => { | 121 | const activityBuilder = (audience: ActivityAudience) => { |
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index a68f03edf..839f66470 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts | |||
@@ -61,7 +61,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod | |||
61 | async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { | 61 | async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { |
62 | logger.info('Creating job to update cache file %s.', redundancyModel.url) | 62 | logger.info('Creating job to update cache file %s.', redundancyModel.url) |
63 | 63 | ||
64 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) | 64 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.getVideo().id) |
65 | 65 | ||
66 | const activityBuilder = (audience: ActivityAudience) => { | 66 | const activityBuilder = (audience: ActivityAudience) => { |
67 | const redundancyObject = redundancyModel.toActivityPubObject() | 67 | const redundancyObject = redundancyModel.toActivityPubObject() |
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts new file mode 100644 index 000000000..8ad126be0 --- /dev/null +++ b/server/lib/activitypub/send/send-view.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { Transaction } from 'sequelize' | ||
2 | import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub' | ||
3 | import { ActorModel } from '../../../models/activitypub/actor' | ||
4 | import { VideoModel } from '../../../models/video/video' | ||
5 | import { getVideoLikeActivityPubUrl } from '../url' | ||
6 | import { sendVideoRelatedActivity } from './utils' | ||
7 | import { audiencify, getAudience } from '../audience' | ||
8 | import { logger } from '../../../helpers/logger' | ||
9 | |||
10 | async function sendView (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
11 | logger.info('Creating job to send view of %s.', video.url) | ||
12 | |||
13 | const activityBuilder = (audience: ActivityAudience) => { | ||
14 | const url = getVideoLikeActivityPubUrl(byActor, video) | ||
15 | |||
16 | return buildViewActivity(url, byActor, video, audience) | ||
17 | } | ||
18 | |||
19 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) | ||
20 | } | ||
21 | |||
22 | function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityView { | ||
23 | if (!audience) audience = getAudience(byActor) | ||
24 | |||
25 | return audiencify( | ||
26 | { | ||
27 | id: url, | ||
28 | type: 'View' as 'View', | ||
29 | actor: byActor.url, | ||
30 | object: video.url | ||
31 | }, | ||
32 | audience | ||
33 | ) | ||
34 | } | ||
35 | |||
36 | // --------------------------------------------------------------------------- | ||
37 | |||
38 | export { | ||
39 | sendView | ||
40 | } | ||
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index 3ff60a97c..1767df0ae 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -4,13 +4,14 @@ import { getServerActor } from '../../helpers/utils' | |||
4 | import { VideoModel } from '../../models/video/video' | 4 | import { VideoModel } from '../../models/video/video' |
5 | import { VideoShareModel } from '../../models/video/video-share' | 5 | import { VideoShareModel } from '../../models/video/video-share' |
6 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' | 6 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' |
7 | import { getAnnounceActivityPubUrl } from './url' | 7 | import { getVideoAnnounceActivityPubUrl } from './url' |
8 | import { VideoChannelModel } from '../../models/video/video-channel' | 8 | import { VideoChannelModel } from '../../models/video/video-channel' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { doRequest } from '../../helpers/requests' | 10 | import { doRequest } from '../../helpers/requests' |
11 | import { getOrCreateActorAndServerAndModel } from './actor' | 11 | import { getOrCreateActorAndServerAndModel } from './actor' |
12 | import { logger } from '../../helpers/logger' | 12 | import { logger } from '../../helpers/logger' |
13 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' | 13 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' |
14 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
14 | 15 | ||
15 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { | 16 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { |
16 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | 17 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined |
@@ -38,9 +39,13 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) { | |||
38 | json: true, | 39 | json: true, |
39 | activityPub: true | 40 | activityPub: true |
40 | }) | 41 | }) |
41 | if (!body || !body.actor) throw new Error('Body of body actor is invalid') | 42 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') |
43 | |||
44 | const actorUrl = getAPId(body.actor) | ||
45 | if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { | ||
46 | throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) | ||
47 | } | ||
42 | 48 | ||
43 | const actorUrl = body.actor | ||
44 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | 49 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) |
45 | 50 | ||
46 | const entry = { | 51 | const entry = { |
@@ -72,8 +77,8 @@ export { | |||
72 | async function shareByServer (video: VideoModel, t: Transaction) { | 77 | async function shareByServer (video: VideoModel, t: Transaction) { |
73 | const serverActor = await getServerActor() | 78 | const serverActor = await getServerActor() |
74 | 79 | ||
75 | const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor) | 80 | const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video) |
76 | return VideoShareModel.findOrCreate({ | 81 | const [ serverShare ] = await VideoShareModel.findOrCreate({ |
77 | defaults: { | 82 | defaults: { |
78 | actorId: serverActor.id, | 83 | actorId: serverActor.id, |
79 | videoId: video.id, | 84 | videoId: video.id, |
@@ -83,16 +88,14 @@ async function shareByServer (video: VideoModel, t: Transaction) { | |||
83 | url: serverShareUrl | 88 | url: serverShareUrl |
84 | }, | 89 | }, |
85 | transaction: t | 90 | transaction: t |
86 | }).then(([ serverShare, created ]) => { | ||
87 | if (created) return sendVideoAnnounce(serverActor, serverShare, video, t) | ||
88 | |||
89 | return undefined | ||
90 | }) | 91 | }) |
92 | |||
93 | return sendVideoAnnounce(serverActor, serverShare, video, t) | ||
91 | } | 94 | } |
92 | 95 | ||
93 | async function shareByVideoChannel (video: VideoModel, t: Transaction) { | 96 | async function shareByVideoChannel (video: VideoModel, t: Transaction) { |
94 | const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor) | 97 | const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video) |
95 | return VideoShareModel.findOrCreate({ | 98 | const [ videoChannelShare ] = await VideoShareModel.findOrCreate({ |
96 | defaults: { | 99 | defaults: { |
97 | actorId: video.VideoChannel.actorId, | 100 | actorId: video.VideoChannel.actorId, |
98 | videoId: video.id, | 101 | videoId: video.id, |
@@ -102,11 +105,9 @@ async function shareByVideoChannel (video: VideoModel, t: Transaction) { | |||
102 | url: videoChannelShareUrl | 105 | url: videoChannelShareUrl |
103 | }, | 106 | }, |
104 | transaction: t | 107 | transaction: t |
105 | }).then(([ videoChannelShare, created ]) => { | ||
106 | if (created) return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) | ||
107 | |||
108 | return undefined | ||
109 | }) | 108 | }) |
109 | |||
110 | return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) | ||
110 | } | 111 | } |
111 | 112 | ||
112 | async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { | 113 | async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { |
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index e792be698..4229fe094 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts | |||
@@ -5,6 +5,8 @@ import { VideoModel } from '../../models/video/video' | |||
5 | import { VideoAbuseModel } from '../../models/video/video-abuse' | 5 | import { VideoAbuseModel } from '../../models/video/video-abuse' |
6 | import { VideoCommentModel } from '../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../models/video/video-comment' |
7 | import { VideoFileModel } from '../../models/video/video-file' | 7 | import { VideoFileModel } from '../../models/video/video-file' |
8 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | ||
9 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
8 | 10 | ||
9 | function getVideoActivityPubUrl (video: VideoModel) { | 11 | function getVideoActivityPubUrl (video: VideoModel) { |
10 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | 12 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid |
@@ -16,6 +18,10 @@ function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) { | |||
16 | return `${CONFIG.WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` | 18 | return `${CONFIG.WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` |
17 | } | 19 | } |
18 | 20 | ||
21 | function getVideoCacheStreamingPlaylistActivityPubUrl (video: VideoModel, playlist: VideoStreamingPlaylistModel) { | ||
22 | return `${CONFIG.WEBSERVER.URL}/redundancy/video-playlists/${playlist.getStringType()}/${video.uuid}` | ||
23 | } | ||
24 | |||
19 | function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { | 25 | function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { |
20 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id | 26 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id |
21 | } | 27 | } |
@@ -33,14 +39,14 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) { | |||
33 | } | 39 | } |
34 | 40 | ||
35 | function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) { | 41 | function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) { |
36 | return video.url + '/views/' + byActor.uuid + '/' + new Date().toISOString() | 42 | return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString() |
37 | } | 43 | } |
38 | 44 | ||
39 | function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { | 45 | function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) { |
40 | return byActor.url + '/likes/' + video.id | 46 | return byActor.url + '/likes/' + video.id |
41 | } | 47 | } |
42 | 48 | ||
43 | function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { | 49 | function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) { |
44 | return byActor.url + '/dislikes/' + video.id | 50 | return byActor.url + '/dislikes/' + video.id |
45 | } | 51 | } |
46 | 52 | ||
@@ -74,8 +80,8 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) { | |||
74 | return follower.url + '/accepts/follows/' + me.id | 80 | return follower.url + '/accepts/follows/' + me.id |
75 | } | 81 | } |
76 | 82 | ||
77 | function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) { | 83 | function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) { |
78 | return originalUrl + '/announces/' + byActor.id | 84 | return video.url + '/announces/' + byActor.id |
79 | } | 85 | } |
80 | 86 | ||
81 | function getDeleteActivityPubUrl (originalUrl: string) { | 87 | function getDeleteActivityPubUrl (originalUrl: string) { |
@@ -92,12 +98,13 @@ function getUndoActivityPubUrl (originalUrl: string) { | |||
92 | 98 | ||
93 | export { | 99 | export { |
94 | getVideoActivityPubUrl, | 100 | getVideoActivityPubUrl, |
101 | getVideoCacheStreamingPlaylistActivityPubUrl, | ||
95 | getVideoChannelActivityPubUrl, | 102 | getVideoChannelActivityPubUrl, |
96 | getAccountActivityPubUrl, | 103 | getAccountActivityPubUrl, |
97 | getVideoAbuseActivityPubUrl, | 104 | getVideoAbuseActivityPubUrl, |
98 | getActorFollowActivityPubUrl, | 105 | getActorFollowActivityPubUrl, |
99 | getActorFollowAcceptActivityPubUrl, | 106 | getActorFollowAcceptActivityPubUrl, |
100 | getAnnounceActivityPubUrl, | 107 | getVideoAnnounceActivityPubUrl, |
101 | getUpdateActivityPubUrl, | 108 | getUpdateActivityPubUrl, |
102 | getUndoActivityPubUrl, | 109 | getUndoActivityPubUrl, |
103 | getVideoViewActivityPubUrl, | 110 | getVideoViewActivityPubUrl, |
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts index c8c17f4c4..e87301fe7 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts | |||
@@ -9,6 +9,7 @@ import { VideoCommentModel } from '../../models/video/video-comment' | |||
9 | import { getOrCreateActorAndServerAndModel } from './actor' | 9 | import { getOrCreateActorAndServerAndModel } from './actor' |
10 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | 10 | import { getOrCreateVideoAndAccountAndChannel } from './videos' |
11 | import * as Bluebird from 'bluebird' | 11 | import * as Bluebird from 'bluebird' |
12 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
12 | 13 | ||
13 | async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) { | 14 | async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) { |
14 | let originCommentId: number = null | 15 | let originCommentId: number = null |
@@ -61,7 +62,15 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) { | |||
61 | const actorUrl = body.attributedTo | 62 | const actorUrl = body.attributedTo |
62 | if (!actorUrl) return { created: false } | 63 | if (!actorUrl) return { created: false } |
63 | 64 | ||
64 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | 65 | if (checkUrlsSameHost(commentUrl, actorUrl) !== true) { |
66 | throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${commentUrl}`) | ||
67 | } | ||
68 | |||
69 | if (checkUrlsSameHost(body.id, commentUrl) !== true) { | ||
70 | throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`) | ||
71 | } | ||
72 | |||
73 | const actor = await getOrCreateActorAndServerAndModel(actorUrl, 'all') | ||
65 | const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) | 74 | const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) |
66 | if (!entry) return { created: false } | 75 | if (!entry) return { created: false } |
67 | 76 | ||
@@ -71,6 +80,8 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) { | |||
71 | }, | 80 | }, |
72 | defaults: entry | 81 | defaults: entry |
73 | }) | 82 | }) |
83 | comment.Account = actor.Account | ||
84 | comment.Video = videoInstance | ||
74 | 85 | ||
75 | return { comment, created } | 86 | return { comment, created } |
76 | } | 87 | } |
@@ -134,6 +145,14 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) { | |||
134 | const actorUrl = body.attributedTo | 145 | const actorUrl = body.attributedTo |
135 | if (!actorUrl) throw new Error('Miss attributed to in comment') | 146 | if (!actorUrl) throw new Error('Miss attributed to in comment') |
136 | 147 | ||
148 | if (checkUrlsSameHost(url, actorUrl) !== true) { | ||
149 | throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`) | ||
150 | } | ||
151 | |||
152 | if (checkUrlsSameHost(body.id, url) !== true) { | ||
153 | throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`) | ||
154 | } | ||
155 | |||
137 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | 156 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) |
138 | const comment = new VideoCommentModel({ | 157 | const comment = new VideoCommentModel({ |
139 | url: body.id, | 158 | url: body.id, |
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index 1619251c3..7aac79118 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts | |||
@@ -1,20 +1,43 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { AccountModel } from '../../models/account/account' | 2 | import { AccountModel } from '../../models/account/account' |
3 | import { VideoModel } from '../../models/video/video' | 3 | import { VideoModel } from '../../models/video/video' |
4 | import { sendCreateDislike, sendLike, sendUndoDislike, sendUndoLike } from './send' | 4 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' |
5 | import { VideoRateType } from '../../../shared/models/videos' | 5 | import { VideoRateType } from '../../../shared/models/videos' |
6 | import * as Bluebird from 'bluebird' | 6 | import * as Bluebird from 'bluebird' |
7 | import { getOrCreateActorAndServerAndModel } from './actor' | 7 | import { getOrCreateActorAndServerAndModel } from './actor' |
8 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 8 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
9 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
10 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' | 10 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' |
11 | import { doRequest } from '../../helpers/requests' | ||
12 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
13 | import { ActorModel } from '../../models/activitypub/actor' | ||
14 | import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url' | ||
15 | import { sendDislike } from './send/send-dislike' | ||
11 | 16 | ||
12 | async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) { | 17 | async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) { |
13 | let rateCounts = 0 | 18 | let rateCounts = 0 |
14 | 19 | ||
15 | await Bluebird.map(actorUrls, async actorUrl => { | 20 | await Bluebird.map(ratesUrl, async rateUrl => { |
16 | try { | 21 | try { |
22 | // Fetch url | ||
23 | const { body } = await doRequest({ | ||
24 | uri: rateUrl, | ||
25 | json: true, | ||
26 | activityPub: true | ||
27 | }) | ||
28 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | ||
29 | |||
30 | const actorUrl = getAPId(body.actor) | ||
31 | if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { | ||
32 | throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) | ||
33 | } | ||
34 | |||
35 | if (checkUrlsSameHost(body.id, rateUrl) !== true) { | ||
36 | throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`) | ||
37 | } | ||
38 | |||
17 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | 39 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) |
40 | |||
18 | const [ , created ] = await AccountVideoRateModel | 41 | const [ , created ] = await AccountVideoRateModel |
19 | .findOrCreate({ | 42 | .findOrCreate({ |
20 | where: { | 43 | where: { |
@@ -24,13 +47,14 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR | |||
24 | defaults: { | 47 | defaults: { |
25 | videoId: video.id, | 48 | videoId: video.id, |
26 | accountId: actor.Account.id, | 49 | accountId: actor.Account.id, |
27 | type: rate | 50 | type: rate, |
51 | url: body.id | ||
28 | } | 52 | } |
29 | }) | 53 | }) |
30 | 54 | ||
31 | if (created) rateCounts += 1 | 55 | if (created) rateCounts += 1 |
32 | } catch (err) { | 56 | } catch (err) { |
33 | logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err }) | 57 | logger.warn('Cannot add rate %s.', rateUrl, { err }) |
34 | } | 58 | } |
35 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | 59 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) |
36 | 60 | ||
@@ -59,10 +83,15 @@ async function sendVideoRateChange (account: AccountModel, | |||
59 | // Like | 83 | // Like |
60 | if (likes > 0) await sendLike(actor, video, t) | 84 | if (likes > 0) await sendLike(actor, video, t) |
61 | // Dislike | 85 | // Dislike |
62 | if (dislikes > 0) await sendCreateDislike(actor, video, t) | 86 | if (dislikes > 0) await sendDislike(actor, video, t) |
87 | } | ||
88 | |||
89 | function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) { | ||
90 | return rateType === 'like' ? getVideoLikeActivityPubUrl(actor, video) : getVideoDislikeActivityPubUrl(actor, video) | ||
63 | } | 91 | } |
64 | 92 | ||
65 | export { | 93 | export { |
94 | getRateUrl, | ||
66 | createRates, | 95 | createRates, |
67 | sendVideoRateChange | 96 | sendVideoRateChange |
68 | } | 97 | } |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 8521572a1..710929aac 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -1,17 +1,23 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as sequelize from 'sequelize' | 2 | import * as sequelize from 'sequelize' |
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import { join } from 'path' | ||
5 | import * as request from 'request' | 4 | import * as request from 'request' |
6 | import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' | 5 | import { |
6 | ActivityIconObject, | ||
7 | ActivityPlaylistSegmentHashesObject, | ||
8 | ActivityPlaylistUrlObject, | ||
9 | ActivityUrlObject, | ||
10 | ActivityVideoUrlObject, | ||
11 | VideoState | ||
12 | } from '../../../shared/index' | ||
7 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 13 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
8 | import { VideoPrivacy } from '../../../shared/models/videos' | 14 | import { VideoPrivacy } from '../../../shared/models/videos' |
9 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' | 15 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' |
10 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 16 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
11 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 17 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
12 | import { logger } from '../../helpers/logger' | 18 | import { logger } from '../../helpers/logger' |
13 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 19 | import { doRequest, downloadImage } from '../../helpers/requests' |
14 | import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers' | 20 | import { ACTIVITY_PUB, CONFIG, MIMETYPES, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers' |
15 | import { ActorModel } from '../../models/activitypub/actor' | 21 | import { ActorModel } from '../../models/activitypub/actor' |
16 | import { TagModel } from '../../models/video/tag' | 22 | import { TagModel } from '../../models/video/tag' |
17 | import { VideoModel } from '../../models/video/video' | 23 | import { VideoModel } from '../../models/video/video' |
@@ -29,6 +35,11 @@ import { createRates } from './video-rates' | |||
29 | import { addVideoShares, shareVideoByServerAndChannel } from './share' | 35 | import { addVideoShares, shareVideoByServerAndChannel } from './share' |
30 | import { AccountModel } from '../../models/account/account' | 36 | import { AccountModel } from '../../models/account/account' |
31 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' | 37 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' |
38 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
39 | import { Notifier } from '../notifier' | ||
40 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
41 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
42 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | ||
32 | 43 | ||
33 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 44 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { |
34 | // If the video is not private and published, we federate it | 45 | // If the video is not private and published, we federate it |
@@ -63,7 +74,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request. | |||
63 | 74 | ||
64 | const { response, body } = await doRequest(options) | 75 | const { response, body } = await doRequest(options) |
65 | 76 | ||
66 | if (sanitizeAndCheckVideoTorrentObject(body) === false) { | 77 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { |
67 | logger.debug('Remote video JSON is not valid.', { body }) | 78 | logger.debug('Remote video JSON is not valid.', { body }) |
68 | return { response, videoObject: undefined } | 79 | return { response, videoObject: undefined } |
69 | } | 80 | } |
@@ -94,19 +105,18 @@ function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Fu | |||
94 | 105 | ||
95 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { | 106 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { |
96 | const thumbnailName = video.getThumbnailName() | 107 | const thumbnailName = video.getThumbnailName() |
97 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) | ||
98 | 108 | ||
99 | const options = { | 109 | return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) |
100 | method: 'GET', | ||
101 | uri: icon.url | ||
102 | } | ||
103 | return doRequestAndSaveToFile(options, thumbnailPath) | ||
104 | } | 110 | } |
105 | 111 | ||
106 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { | 112 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { |
107 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') | 113 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') |
108 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) | 114 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) |
109 | 115 | ||
116 | if (checkUrlsSameHost(channel.id, videoObject.id) !== true) { | ||
117 | throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`) | ||
118 | } | ||
119 | |||
110 | return getOrCreateActorAndServerAndModel(channel.id, 'all') | 120 | return getOrCreateActorAndServerAndModel(channel.id, 'all') |
111 | } | 121 | } |
112 | 122 | ||
@@ -116,7 +126,7 @@ type SyncParam = { | |||
116 | shares: boolean | 126 | shares: boolean |
117 | comments: boolean | 127 | comments: boolean |
118 | thumbnail: boolean | 128 | thumbnail: boolean |
119 | refreshVideo: boolean | 129 | refreshVideo?: boolean |
120 | } | 130 | } |
121 | async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { | 131 | async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { |
122 | logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) | 132 | logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) |
@@ -155,31 +165,34 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid | |||
155 | } | 165 | } |
156 | 166 | ||
157 | async function getOrCreateVideoAndAccountAndChannel (options: { | 167 | async function getOrCreateVideoAndAccountAndChannel (options: { |
158 | videoObject: VideoTorrentObject | string, | 168 | videoObject: { id: string } | string, |
159 | syncParam?: SyncParam, | 169 | syncParam?: SyncParam, |
160 | fetchType?: VideoFetchByUrlType, | 170 | fetchType?: VideoFetchByUrlType, |
161 | refreshViews?: boolean | 171 | allowRefresh?: boolean // true by default |
162 | }) { | 172 | }) { |
163 | // Default params | 173 | // Default params |
164 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } | 174 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } |
165 | const fetchType = options.fetchType || 'all' | 175 | const fetchType = options.fetchType || 'all' |
166 | const refreshViews = options.refreshViews || false | 176 | const allowRefresh = options.allowRefresh !== false |
167 | 177 | ||
168 | // Get video url | 178 | // Get video url |
169 | const videoUrl = typeof options.videoObject === 'string' ? options.videoObject : options.videoObject.id | 179 | const videoUrl = getAPId(options.videoObject) |
170 | 180 | ||
171 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) | 181 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) |
172 | if (videoFromDatabase) { | 182 | if (videoFromDatabase) { |
173 | const refreshOptions = { | 183 | |
174 | video: videoFromDatabase, | 184 | if (allowRefresh === true) { |
175 | fetchedType: fetchType, | 185 | const refreshOptions = { |
176 | syncParam, | 186 | video: videoFromDatabase, |
177 | refreshViews | 187 | fetchedType: fetchType, |
188 | syncParam | ||
189 | } | ||
190 | |||
191 | if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) | ||
192 | else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } }) | ||
178 | } | 193 | } |
179 | const p = refreshVideoIfNeeded(refreshOptions) | ||
180 | if (syncParam.refreshVideo === true) videoFromDatabase = await p | ||
181 | 194 | ||
182 | return { video: videoFromDatabase } | 195 | return { video: videoFromDatabase, created: false } |
183 | } | 196 | } |
184 | 197 | ||
185 | const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) | 198 | const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) |
@@ -190,7 +203,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: { | |||
190 | 203 | ||
191 | await syncVideoExternalAttributes(video, fetchedVideo, syncParam) | 204 | await syncVideoExternalAttributes(video, fetchedVideo, syncParam) |
192 | 205 | ||
193 | return { video } | 206 | return { video, created: true } |
194 | } | 207 | } |
195 | 208 | ||
196 | async function updateVideoFromAP (options: { | 209 | async function updateVideoFromAP (options: { |
@@ -198,17 +211,17 @@ async function updateVideoFromAP (options: { | |||
198 | videoObject: VideoTorrentObject, | 211 | videoObject: VideoTorrentObject, |
199 | account: AccountModel, | 212 | account: AccountModel, |
200 | channel: VideoChannelModel, | 213 | channel: VideoChannelModel, |
201 | updateViews: boolean, | ||
202 | overrideTo?: string[] | 214 | overrideTo?: string[] |
203 | }) { | 215 | }) { |
204 | logger.debug('Updating remote video "%s".', options.videoObject.uuid) | 216 | logger.debug('Updating remote video "%s".', options.videoObject.uuid) |
217 | |||
205 | let videoFieldsSave: any | 218 | let videoFieldsSave: any |
219 | const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE | ||
220 | const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED | ||
206 | 221 | ||
207 | try { | 222 | try { |
208 | await sequelizeTypescript.transaction(async t => { | 223 | await sequelizeTypescript.transaction(async t => { |
209 | const sequelizeOptions = { | 224 | const sequelizeOptions = { transaction: t } |
210 | transaction: t | ||
211 | } | ||
212 | 225 | ||
213 | videoFieldsSave = options.video.toJSON() | 226 | videoFieldsSave = options.video.toJSON() |
214 | 227 | ||
@@ -238,14 +251,10 @@ async function updateVideoFromAP (options: { | |||
238 | options.video.set('publishedAt', videoData.publishedAt) | 251 | options.video.set('publishedAt', videoData.publishedAt) |
239 | options.video.set('privacy', videoData.privacy) | 252 | options.video.set('privacy', videoData.privacy) |
240 | options.video.set('channelId', videoData.channelId) | 253 | options.video.set('channelId', videoData.channelId) |
254 | options.video.set('views', videoData.views) | ||
241 | 255 | ||
242 | if (options.updateViews === true) options.video.set('views', videoData.views) | ||
243 | await options.video.save(sequelizeOptions) | 256 | await options.video.save(sequelizeOptions) |
244 | 257 | ||
245 | // Don't block on request | ||
246 | generateThumbnailFromUrl(options.video, options.videoObject.icon) | ||
247 | .catch(err => logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })) | ||
248 | |||
249 | { | 258 | { |
250 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) | 259 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) |
251 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | 260 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) |
@@ -266,6 +275,25 @@ async function updateVideoFromAP (options: { | |||
266 | } | 275 | } |
267 | 276 | ||
268 | { | 277 | { |
278 | const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(options.video, options.videoObject) | ||
279 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) | ||
280 | |||
281 | // Remove video files that do not exist anymore | ||
282 | const destroyTasks = options.video.VideoStreamingPlaylists | ||
283 | .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f))) | ||
284 | .map(f => f.destroy(sequelizeOptions)) | ||
285 | await Promise.all(destroyTasks) | ||
286 | |||
287 | // Update or add other one | ||
288 | const upsertTasks = streamingPlaylistAttributes.map(a => { | ||
289 | return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t }) | ||
290 | .then(([ streamingPlaylist ]) => streamingPlaylist) | ||
291 | }) | ||
292 | |||
293 | options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks) | ||
294 | } | ||
295 | |||
296 | { | ||
269 | // Update Tags | 297 | // Update Tags |
270 | const tags = options.videoObject.tag.map(tag => tag.name) | 298 | const tags = options.videoObject.tag.map(tag => tag.name) |
271 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 299 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
@@ -283,6 +311,11 @@ async function updateVideoFromAP (options: { | |||
283 | } | 311 | } |
284 | }) | 312 | }) |
285 | 313 | ||
314 | // Notify our users? | ||
315 | if (wasPrivateVideo || wasUnlistedVideo) { | ||
316 | Notifier.Instance.notifyOnNewVideo(options.video) | ||
317 | } | ||
318 | |||
286 | logger.info('Remote video with uuid %s updated', options.videoObject.uuid) | 319 | logger.info('Remote video with uuid %s updated', options.videoObject.uuid) |
287 | } catch (err) { | 320 | } catch (err) { |
288 | if (options.video !== undefined && videoFieldsSave !== undefined) { | 321 | if (options.video !== undefined && videoFieldsSave !== undefined) { |
@@ -293,10 +326,66 @@ async function updateVideoFromAP (options: { | |||
293 | logger.debug('Cannot update the remote video.', { err }) | 326 | logger.debug('Cannot update the remote video.', { err }) |
294 | throw err | 327 | throw err |
295 | } | 328 | } |
329 | |||
330 | try { | ||
331 | await generateThumbnailFromUrl(options.video, options.videoObject.icon) | ||
332 | } catch (err) { | ||
333 | logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err }) | ||
334 | } | ||
335 | } | ||
336 | |||
337 | async function refreshVideoIfNeeded (options: { | ||
338 | video: VideoModel, | ||
339 | fetchedType: VideoFetchByUrlType, | ||
340 | syncParam: SyncParam | ||
341 | }): Promise<VideoModel> { | ||
342 | if (!options.video.isOutdated()) return options.video | ||
343 | |||
344 | // We need more attributes if the argument video was fetched with not enough joints | ||
345 | const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) | ||
346 | |||
347 | try { | ||
348 | const { response, videoObject } = await fetchRemoteVideo(video.url) | ||
349 | if (response.statusCode === 404) { | ||
350 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
351 | |||
352 | // Video does not exist anymore | ||
353 | await video.destroy() | ||
354 | return undefined | ||
355 | } | ||
356 | |||
357 | if (videoObject === undefined) { | ||
358 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) | ||
359 | |||
360 | await video.setAsRefreshed() | ||
361 | return video | ||
362 | } | ||
363 | |||
364 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) | ||
365 | const account = await AccountModel.load(channelActor.VideoChannel.accountId) | ||
366 | |||
367 | const updateOptions = { | ||
368 | video, | ||
369 | videoObject, | ||
370 | account, | ||
371 | channel: channelActor.VideoChannel | ||
372 | } | ||
373 | await retryTransactionWrapper(updateVideoFromAP, updateOptions) | ||
374 | await syncVideoExternalAttributes(video, videoObject, options.syncParam) | ||
375 | |||
376 | return video | ||
377 | } catch (err) { | ||
378 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) | ||
379 | |||
380 | // Don't refresh in loop | ||
381 | await video.setAsRefreshed() | ||
382 | return video | ||
383 | } | ||
296 | } | 384 | } |
297 | 385 | ||
298 | export { | 386 | export { |
299 | updateVideoFromAP, | 387 | updateVideoFromAP, |
388 | refreshVideoIfNeeded, | ||
300 | federateVideoIfNeeded, | 389 | federateVideoIfNeeded, |
301 | fetchRemoteVideo, | 390 | fetchRemoteVideo, |
302 | getOrCreateVideoAndAccountAndChannel, | 391 | getOrCreateVideoAndAccountAndChannel, |
@@ -308,10 +397,23 @@ export { | |||
308 | 397 | ||
309 | // --------------------------------------------------------------------------- | 398 | // --------------------------------------------------------------------------- |
310 | 399 | ||
311 | function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { | 400 | function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { |
312 | const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) | 401 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) |
313 | 402 | ||
314 | return mimeTypes.indexOf(url.mimeType) !== -1 && url.mimeType.startsWith('video/') | 403 | const urlMediaType = url.mediaType || url.mimeType |
404 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') | ||
405 | } | ||
406 | |||
407 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { | ||
408 | const urlMediaType = url.mediaType || url.mimeType | ||
409 | |||
410 | return urlMediaType === 'application/x-mpegURL' | ||
411 | } | ||
412 | |||
413 | function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { | ||
414 | const urlMediaType = tag.mediaType || tag.mimeType | ||
415 | |||
416 | return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json' | ||
315 | } | 417 | } |
316 | 418 | ||
317 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { | 419 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { |
@@ -334,8 +436,14 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor | |||
334 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) | 436 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) |
335 | await Promise.all(videoFilePromises) | 437 | await Promise.all(videoFilePromises) |
336 | 438 | ||
439 | const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject) | ||
440 | const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t })) | ||
441 | await Promise.all(playlistPromises) | ||
442 | |||
337 | // Process tags | 443 | // Process tags |
338 | const tags = videoObject.tag.map(t => t.name) | 444 | const tags = videoObject.tag |
445 | .filter(t => t.type === 'Hashtag') | ||
446 | .map(t => t.name) | ||
339 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 447 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
340 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) | 448 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) |
341 | 449 | ||
@@ -359,52 +467,6 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor | |||
359 | return videoCreated | 467 | return videoCreated |
360 | } | 468 | } |
361 | 469 | ||
362 | async function refreshVideoIfNeeded (options: { | ||
363 | video: VideoModel, | ||
364 | fetchedType: VideoFetchByUrlType, | ||
365 | syncParam: SyncParam, | ||
366 | refreshViews: boolean | ||
367 | }): Promise<VideoModel> { | ||
368 | if (!options.video.isOutdated()) return options.video | ||
369 | |||
370 | // We need more attributes if the argument video was fetched with not enough joints | ||
371 | const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) | ||
372 | |||
373 | try { | ||
374 | const { response, videoObject } = await fetchRemoteVideo(video.url) | ||
375 | if (response.statusCode === 404) { | ||
376 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
377 | |||
378 | // Video does not exist anymore | ||
379 | await video.destroy() | ||
380 | return undefined | ||
381 | } | ||
382 | |||
383 | if (videoObject === undefined) { | ||
384 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) | ||
385 | return video | ||
386 | } | ||
387 | |||
388 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) | ||
389 | const account = await AccountModel.load(channelActor.VideoChannel.accountId) | ||
390 | |||
391 | const updateOptions = { | ||
392 | video, | ||
393 | videoObject, | ||
394 | account, | ||
395 | channel: channelActor.VideoChannel, | ||
396 | updateViews: options.refreshViews | ||
397 | } | ||
398 | await retryTransactionWrapper(updateVideoFromAP, updateOptions) | ||
399 | await syncVideoExternalAttributes(video, videoObject, options.syncParam) | ||
400 | |||
401 | return video | ||
402 | } catch (err) { | ||
403 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) | ||
404 | return video | ||
405 | } | ||
406 | } | ||
407 | |||
408 | async function videoActivityObjectToDBAttributes ( | 470 | async function videoActivityObjectToDBAttributes ( |
409 | videoChannel: VideoChannelModel, | 471 | videoChannel: VideoChannelModel, |
410 | videoObject: VideoTorrentObject, | 472 | videoObject: VideoTorrentObject, |
@@ -460,17 +522,18 @@ async function videoActivityObjectToDBAttributes ( | |||
460 | } | 522 | } |
461 | 523 | ||
462 | function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { | 524 | function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { |
463 | const fileUrls = videoObject.url.filter(u => isActivityVideoUrlObject(u)) as ActivityVideoUrlObject[] | 525 | const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] |
464 | 526 | ||
465 | if (fileUrls.length === 0) { | 527 | if (fileUrls.length === 0) { |
466 | throw new Error('Cannot find video files for ' + video.url) | 528 | throw new Error('Cannot find video files for ' + video.url) |
467 | } | 529 | } |
468 | 530 | ||
469 | const attributes: VideoFileModel[] = [] | 531 | const attributes: FilteredModelAttributes<VideoFileModel>[] = [] |
470 | for (const fileUrl of fileUrls) { | 532 | for (const fileUrl of fileUrls) { |
471 | // Fetch associated magnet uri | 533 | // Fetch associated magnet uri |
472 | const magnet = videoObject.url.find(u => { | 534 | const magnet = videoObject.url.find(u => { |
473 | return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.height === fileUrl.height | 535 | const mediaType = u.mediaType || u.mimeType |
536 | return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height | ||
474 | }) | 537 | }) |
475 | 538 | ||
476 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) | 539 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) |
@@ -480,14 +543,53 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid | |||
480 | throw new Error('Cannot parse magnet URI ' + magnet.href) | 543 | throw new Error('Cannot parse magnet URI ' + magnet.href) |
481 | } | 544 | } |
482 | 545 | ||
546 | const mediaType = fileUrl.mediaType || fileUrl.mimeType | ||
483 | const attribute = { | 547 | const attribute = { |
484 | extname: VIDEO_MIMETYPE_EXT[ fileUrl.mimeType ], | 548 | extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], |
485 | infoHash: parsed.infoHash, | 549 | infoHash: parsed.infoHash, |
486 | resolution: fileUrl.height, | 550 | resolution: fileUrl.height, |
487 | size: fileUrl.size, | 551 | size: fileUrl.size, |
488 | videoId: video.id, | 552 | videoId: video.id, |
489 | fps: fileUrl.fps || -1 | 553 | fps: fileUrl.fps || -1 |
490 | } as VideoFileModel | 554 | } |
555 | |||
556 | attributes.push(attribute) | ||
557 | } | ||
558 | |||
559 | return attributes | ||
560 | } | ||
561 | |||
562 | function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { | ||
563 | const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] | ||
564 | if (playlistUrls.length === 0) return [] | ||
565 | |||
566 | const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = [] | ||
567 | for (const playlistUrlObject of playlistUrls) { | ||
568 | const p2pMediaLoaderInfohashes = playlistUrlObject.tag | ||
569 | .filter(t => t.type === 'Infohash') | ||
570 | .map(t => t.name) | ||
571 | if (p2pMediaLoaderInfohashes.length === 0) { | ||
572 | logger.warn('No infohashes found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
573 | continue | ||
574 | } | ||
575 | |||
576 | const segmentsSha256UrlObject = playlistUrlObject.tag | ||
577 | .find(t => { | ||
578 | return isAPPlaylistSegmentHashesUrlObject(t) | ||
579 | }) as ActivityPlaylistSegmentHashesObject | ||
580 | if (!segmentsSha256UrlObject) { | ||
581 | logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
582 | continue | ||
583 | } | ||
584 | |||
585 | const attribute = { | ||
586 | type: VideoStreamingPlaylistType.HLS, | ||
587 | playlistUrl: playlistUrlObject.href, | ||
588 | segmentsSha256Url: segmentsSha256UrlObject.href, | ||
589 | p2pMediaLoaderInfohashes, | ||
590 | videoId: video.id | ||
591 | } | ||
592 | |||
491 | attributes.push(attribute) | 593 | attributes.push(attribute) |
492 | } | 594 | } |
493 | 595 | ||
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts index 4b6bc3185..021426a1a 100644 --- a/server/lib/avatar.ts +++ b/server/lib/avatar.ts | |||
@@ -7,10 +7,11 @@ import { AccountModel } from '../models/account/account' | |||
7 | import { VideoChannelModel } from '../models/video/video-channel' | 7 | import { VideoChannelModel } from '../models/video/video-channel' |
8 | import { extname, join } from 'path' | 8 | import { extname, join } from 'path' |
9 | import { retryTransactionWrapper } from '../helpers/database-utils' | 9 | import { retryTransactionWrapper } from '../helpers/database-utils' |
10 | import * as uuidv4 from 'uuid/v4' | ||
10 | 11 | ||
11 | async function updateActorAvatarFile (avatarPhysicalFile: Express.Multer.File, accountOrChannel: AccountModel | VideoChannelModel) { | 12 | async function updateActorAvatarFile (avatarPhysicalFile: Express.Multer.File, accountOrChannel: AccountModel | VideoChannelModel) { |
12 | const extension = extname(avatarPhysicalFile.filename) | 13 | const extension = extname(avatarPhysicalFile.filename) |
13 | const avatarName = accountOrChannel.Actor.uuid + extension | 14 | const avatarName = uuidv4() + extension |
14 | const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) | 15 | const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) |
15 | await processImage(avatarPhysicalFile, destination, AVATARS_SIZE) | 16 | await processImage(avatarPhysicalFile, destination, AVATARS_SIZE) |
16 | 17 | ||
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts new file mode 100644 index 000000000..1633e500c --- /dev/null +++ b/server/lib/blocklist.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { sequelizeTypescript } from '../initializers' | ||
2 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | ||
3 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | ||
4 | |||
5 | function addAccountInBlocklist (byAccountId: number, targetAccountId: number) { | ||
6 | return sequelizeTypescript.transaction(async t => { | ||
7 | return AccountBlocklistModel.upsert({ | ||
8 | accountId: byAccountId, | ||
9 | targetAccountId: targetAccountId | ||
10 | }, { transaction: t }) | ||
11 | }) | ||
12 | } | ||
13 | |||
14 | function addServerInBlocklist (byAccountId: number, targetServerId: number) { | ||
15 | return sequelizeTypescript.transaction(async t => { | ||
16 | return ServerBlocklistModel.upsert({ | ||
17 | accountId: byAccountId, | ||
18 | targetServerId | ||
19 | }, { transaction: t }) | ||
20 | }) | ||
21 | } | ||
22 | |||
23 | function removeAccountFromBlocklist (accountBlock: AccountBlocklistModel) { | ||
24 | return sequelizeTypescript.transaction(async t => { | ||
25 | return accountBlock.destroy({ transaction: t }) | ||
26 | }) | ||
27 | } | ||
28 | |||
29 | function removeServerFromBlocklist (serverBlock: ServerBlocklistModel) { | ||
30 | return sequelizeTypescript.transaction(async t => { | ||
31 | return serverBlock.destroy({ transaction: t }) | ||
32 | }) | ||
33 | } | ||
34 | |||
35 | export { | ||
36 | addAccountInBlocklist, | ||
37 | addServerInBlocklist, | ||
38 | removeAccountFromBlocklist, | ||
39 | removeServerFromBlocklist | ||
40 | } | ||
diff --git a/server/lib/cache/actor-follow-score-cache.ts b/server/lib/cache/actor-follow-score-cache.ts new file mode 100644 index 000000000..d070bde09 --- /dev/null +++ b/server/lib/cache/actor-follow-score-cache.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | import { ACTOR_FOLLOW_SCORE } from '../../initializers' | ||
2 | import { logger } from '../../helpers/logger' | ||
3 | |||
4 | // Cache follows scores, instead of writing them too often in database | ||
5 | // Keep data in memory, we don't really need Redis here as we don't really care to loose some scores | ||
6 | class ActorFollowScoreCache { | ||
7 | |||
8 | private static instance: ActorFollowScoreCache | ||
9 | private pendingFollowsScore: { [ url: string ]: number } = {} | ||
10 | |||
11 | private constructor () {} | ||
12 | |||
13 | static get Instance () { | ||
14 | return this.instance || (this.instance = new this()) | ||
15 | } | ||
16 | |||
17 | updateActorFollowsScore (goodInboxes: string[], badInboxes: string[]) { | ||
18 | if (goodInboxes.length === 0 && badInboxes.length === 0) return | ||
19 | |||
20 | logger.info('Updating %d good actor follows and %d bad actor follows scores in cache.', goodInboxes.length, badInboxes.length) | ||
21 | |||
22 | for (const goodInbox of goodInboxes) { | ||
23 | if (this.pendingFollowsScore[goodInbox] === undefined) this.pendingFollowsScore[goodInbox] = 0 | ||
24 | |||
25 | this.pendingFollowsScore[goodInbox] += ACTOR_FOLLOW_SCORE.BONUS | ||
26 | } | ||
27 | |||
28 | for (const badInbox of badInboxes) { | ||
29 | if (this.pendingFollowsScore[badInbox] === undefined) this.pendingFollowsScore[badInbox] = 0 | ||
30 | |||
31 | this.pendingFollowsScore[badInbox] += ACTOR_FOLLOW_SCORE.PENALTY | ||
32 | } | ||
33 | } | ||
34 | |||
35 | getPendingFollowsScoreCopy () { | ||
36 | return this.pendingFollowsScore | ||
37 | } | ||
38 | |||
39 | clearPendingFollowsScore () { | ||
40 | this.pendingFollowsScore = {} | ||
41 | } | ||
42 | } | ||
43 | |||
44 | export { | ||
45 | ActorFollowScoreCache | ||
46 | } | ||
diff --git a/server/lib/cache/index.ts b/server/lib/cache/index.ts index 54eb983fa..e921d04a7 100644 --- a/server/lib/cache/index.ts +++ b/server/lib/cache/index.ts | |||
@@ -1,2 +1,3 @@ | |||
1 | export * from './actor-follow-score-cache' | ||
1 | export * from './videos-preview-cache' | 2 | export * from './videos-preview-cache' |
2 | export * from './videos-caption-cache' | 3 | export * from './videos-caption-cache' |
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index fc013e0c3..b2c376e20 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as Bluebird from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
3 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' | 3 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' |
4 | import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, STATIC_PATHS } from '../initializers' | 4 | import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE } from '../initializers' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { escapeHTML } from '../helpers/core-utils' | 6 | import { escapeHTML } from '../helpers/core-utils' |
7 | import { VideoModel } from '../models/video/video' | 7 | import { VideoModel } from '../models/video/video' |
@@ -18,21 +18,13 @@ export class ClientHtml { | |||
18 | ClientHtml.htmlCache = {} | 18 | ClientHtml.htmlCache = {} |
19 | } | 19 | } |
20 | 20 | ||
21 | static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) { | 21 | static async getDefaultHTMLPage (req: express.Request, res: express.Response, paramLang?: string) { |
22 | const path = ClientHtml.getIndexPath(req, res, paramLang) | 22 | const html = await ClientHtml.getIndexHTML(req, res, paramLang) |
23 | if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] | ||
24 | |||
25 | const buffer = await readFile(path) | ||
26 | 23 | ||
27 | let html = buffer.toString() | 24 | let customHtml = ClientHtml.addTitleTag(html) |
28 | 25 | customHtml = ClientHtml.addDescriptionTag(customHtml) | |
29 | html = ClientHtml.addTitleTag(html) | ||
30 | html = ClientHtml.addDescriptionTag(html) | ||
31 | html = ClientHtml.addCustomCSS(html) | ||
32 | 26 | ||
33 | ClientHtml.htmlCache[path] = html | 27 | return customHtml |
34 | |||
35 | return html | ||
36 | } | 28 | } |
37 | 29 | ||
38 | static async getWatchHTMLPage (videoId: string, req: express.Request, res: express.Response) { | 30 | static async getWatchHTMLPage (videoId: string, req: express.Request, res: express.Response) { |
@@ -55,7 +47,26 @@ export class ClientHtml { | |||
55 | return ClientHtml.getIndexHTML(req, res) | 47 | return ClientHtml.getIndexHTML(req, res) |
56 | } | 48 | } |
57 | 49 | ||
58 | return ClientHtml.addOpenGraphAndOEmbedTags(html, video) | 50 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(video.name)) |
51 | customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(video.description)) | ||
52 | customHtml = ClientHtml.addOpenGraphAndOEmbedTags(customHtml, video) | ||
53 | |||
54 | return customHtml | ||
55 | } | ||
56 | |||
57 | private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) { | ||
58 | const path = ClientHtml.getIndexPath(req, res, paramLang) | ||
59 | if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] | ||
60 | |||
61 | const buffer = await readFile(path) | ||
62 | |||
63 | let html = buffer.toString() | ||
64 | |||
65 | html = ClientHtml.addCustomCSS(html) | ||
66 | |||
67 | ClientHtml.htmlCache[path] = html | ||
68 | |||
69 | return html | ||
59 | } | 70 | } |
60 | 71 | ||
61 | private static getIndexPath (req: express.Request, res: express.Response, paramLang?: string) { | 72 | private static getIndexPath (req: express.Request, res: express.Response, paramLang?: string) { |
@@ -81,14 +92,18 @@ export class ClientHtml { | |||
81 | return join(__dirname, '../../../client/dist/' + buildFileLocale(lang) + '/index.html') | 92 | return join(__dirname, '../../../client/dist/' + buildFileLocale(lang) + '/index.html') |
82 | } | 93 | } |
83 | 94 | ||
84 | private static addTitleTag (htmlStringPage: string) { | 95 | private static addTitleTag (htmlStringPage: string, title?: string) { |
85 | const titleTag = '<title>' + CONFIG.INSTANCE.NAME + '</title>' | 96 | let text = title || CONFIG.INSTANCE.NAME |
97 | if (title) text += ` - ${CONFIG.INSTANCE.NAME}` | ||
98 | |||
99 | const titleTag = `<title>${text}</title>` | ||
86 | 100 | ||
87 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.TITLE, titleTag) | 101 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.TITLE, titleTag) |
88 | } | 102 | } |
89 | 103 | ||
90 | private static addDescriptionTag (htmlStringPage: string) { | 104 | private static addDescriptionTag (htmlStringPage: string, description?: string) { |
91 | const descriptionTag = `<meta name="description" content="${CONFIG.INSTANCE.SHORT_DESCRIPTION}" />` | 105 | const content = description || CONFIG.INSTANCE.SHORT_DESCRIPTION |
106 | const descriptionTag = `<meta name="description" content="${content}" />` | ||
92 | 107 | ||
93 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.DESCRIPTION, descriptionTag) | 108 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.DESCRIPTION, descriptionTag) |
94 | } | 109 | } |
@@ -100,8 +115,8 @@ export class ClientHtml { | |||
100 | } | 115 | } |
101 | 116 | ||
102 | private static addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { | 117 | private static addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { |
103 | const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName() | 118 | const previewUrl = CONFIG.WEBSERVER.URL + video.getPreviewStaticPath() |
104 | const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | 119 | const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() |
105 | 120 | ||
106 | const videoNameEscaped = escapeHTML(video.name) | 121 | const videoNameEscaped = escapeHTML(video.name) |
107 | const videoDescriptionEscaped = escapeHTML(video.description) | 122 | const videoDescriptionEscaped = escapeHTML(video.description) |
@@ -172,8 +187,8 @@ export class ClientHtml { | |||
172 | // Schema.org | 187 | // Schema.org |
173 | tagsString += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>` | 188 | tagsString += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>` |
174 | 189 | ||
175 | // SEO | 190 | // SEO, use origin video url so Google does not index remote videos |
176 | tagsString += `<link rel="canonical" href="${videoUrl}" />` | 191 | tagsString += `<link rel="canonical" href="${video.url}" />` |
177 | 192 | ||
178 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.OPENGRAPH_AND_OEMBED, tagsString) | 193 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.OPENGRAPH_AND_OEMBED, tagsString) |
179 | } | 194 | } |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 9327792fb..672414cc0 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { createTransport, Transporter } from 'nodemailer' | 1 | import { createTransport, Transporter } from 'nodemailer' |
2 | import { UserRight } from '../../shared/models/users' | ||
3 | import { isTestInstance } from '../helpers/core-utils' | 2 | import { isTestInstance } from '../helpers/core-utils' |
4 | import { bunyanLogger, logger } from '../helpers/logger' | 3 | import { bunyanLogger, logger } from '../helpers/logger' |
5 | import { CONFIG } from '../initializers' | 4 | import { CONFIG } from '../initializers' |
@@ -8,6 +7,11 @@ import { VideoModel } from '../models/video/video' | |||
8 | import { JobQueue } from './job-queue' | 7 | import { JobQueue } from './job-queue' |
9 | import { EmailPayload } from './job-queue/handlers/email' | 8 | import { EmailPayload } from './job-queue/handlers/email' |
10 | import { readFileSync } from 'fs-extra' | 9 | import { readFileSync } from 'fs-extra' |
10 | import { VideoCommentModel } from '../models/video/video-comment' | ||
11 | import { VideoAbuseModel } from '../models/video/video-abuse' | ||
12 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | ||
13 | import { VideoImportModel } from '../models/video/video-import' | ||
14 | import { ActorFollowModel } from '../models/activitypub/actor-follow' | ||
11 | 15 | ||
12 | class Emailer { | 16 | class Emailer { |
13 | 17 | ||
@@ -22,7 +26,7 @@ class Emailer { | |||
22 | if (this.initialized === true) return | 26 | if (this.initialized === true) return |
23 | this.initialized = true | 27 | this.initialized = true |
24 | 28 | ||
25 | if (CONFIG.SMTP.HOSTNAME && CONFIG.SMTP.PORT) { | 29 | if (Emailer.isEnabled()) { |
26 | logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) | 30 | logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) |
27 | 31 | ||
28 | let tls | 32 | let tls |
@@ -57,6 +61,10 @@ class Emailer { | |||
57 | } | 61 | } |
58 | } | 62 | } |
59 | 63 | ||
64 | static isEnabled () { | ||
65 | return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT | ||
66 | } | ||
67 | |||
60 | async checkConnectionOrDie () { | 68 | async checkConnectionOrDie () { |
61 | if (!this.transporter) return | 69 | if (!this.transporter) return |
62 | 70 | ||
@@ -72,50 +80,158 @@ class Emailer { | |||
72 | } | 80 | } |
73 | } | 81 | } |
74 | 82 | ||
75 | addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) { | 83 | addNewVideoFromSubscriberNotification (to: string[], video: VideoModel) { |
84 | const channelName = video.VideoChannel.getDisplayName() | ||
85 | const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() | ||
86 | |||
76 | const text = `Hi dear user,\n\n` + | 87 | const text = `Hi dear user,\n\n` + |
77 | `It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` + | 88 | `Your subscription ${channelName} just published a new video: ${video.name}` + |
78 | `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + | 89 | `\n\n` + |
79 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | 90 | `You can view it on ${videoUrl} ` + |
91 | `\n\n` + | ||
80 | `Cheers,\n` + | 92 | `Cheers,\n` + |
81 | `PeerTube.` | 93 | `PeerTube.` |
82 | 94 | ||
83 | const emailPayload: EmailPayload = { | 95 | const emailPayload: EmailPayload = { |
84 | to: [ to ], | 96 | to, |
85 | subject: 'Reset your PeerTube password', | 97 | subject: channelName + ' just published a new video', |
86 | text | 98 | text |
87 | } | 99 | } |
88 | 100 | ||
89 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 101 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
90 | } | 102 | } |
91 | 103 | ||
92 | addVerifyEmailJob (to: string, verifyEmailUrl: string) { | 104 | addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') { |
93 | const text = `Welcome to PeerTube,\n\n` + | 105 | const followerName = actorFollow.ActorFollower.Account.getDisplayName() |
94 | `To start using PeerTube on ${CONFIG.WEBSERVER.HOST} you must verify your email! ` + | 106 | const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() |
95 | `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` + | 107 | |
96 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | 108 | const text = `Hi dear user,\n\n` + |
109 | `Your ${followType} ${followingName} has a new subscriber: ${followerName}` + | ||
110 | `\n\n` + | ||
97 | `Cheers,\n` + | 111 | `Cheers,\n` + |
98 | `PeerTube.` | 112 | `PeerTube.` |
99 | 113 | ||
100 | const emailPayload: EmailPayload = { | 114 | const emailPayload: EmailPayload = { |
101 | to: [ to ], | 115 | to, |
102 | subject: 'Verify your PeerTube email', | 116 | subject: 'New follower on your channel ' + followingName, |
117 | text | ||
118 | } | ||
119 | |||
120 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
121 | } | ||
122 | |||
123 | myVideoPublishedNotification (to: string[], video: VideoModel) { | ||
124 | const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() | ||
125 | |||
126 | const text = `Hi dear user,\n\n` + | ||
127 | `Your video ${video.name} has been published.` + | ||
128 | `\n\n` + | ||
129 | `You can view it on ${videoUrl} ` + | ||
130 | `\n\n` + | ||
131 | `Cheers,\n` + | ||
132 | `PeerTube.` | ||
133 | |||
134 | const emailPayload: EmailPayload = { | ||
135 | to, | ||
136 | subject: `Your video ${video.name} is published`, | ||
137 | text | ||
138 | } | ||
139 | |||
140 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
141 | } | ||
142 | |||
143 | myVideoImportSuccessNotification (to: string[], videoImport: VideoImportModel) { | ||
144 | const videoUrl = CONFIG.WEBSERVER.URL + videoImport.Video.getWatchStaticPath() | ||
145 | |||
146 | const text = `Hi dear user,\n\n` + | ||
147 | `Your video import ${videoImport.getTargetIdentifier()} is finished.` + | ||
148 | `\n\n` + | ||
149 | `You can view the imported video on ${videoUrl} ` + | ||
150 | `\n\n` + | ||
151 | `Cheers,\n` + | ||
152 | `PeerTube.` | ||
153 | |||
154 | const emailPayload: EmailPayload = { | ||
155 | to, | ||
156 | subject: `Your video import ${videoImport.getTargetIdentifier()} is finished`, | ||
157 | text | ||
158 | } | ||
159 | |||
160 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
161 | } | ||
162 | |||
163 | myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) { | ||
164 | const importUrl = CONFIG.WEBSERVER.URL + '/my-account/video-imports' | ||
165 | |||
166 | const text = `Hi dear user,\n\n` + | ||
167 | `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` + | ||
168 | `\n\n` + | ||
169 | `See your videos import dashboard for more information: ${importUrl}` + | ||
170 | `\n\n` + | ||
171 | `Cheers,\n` + | ||
172 | `PeerTube.` | ||
173 | |||
174 | const emailPayload: EmailPayload = { | ||
175 | to, | ||
176 | subject: `Your video import ${videoImport.getTargetIdentifier()} encountered an error`, | ||
177 | text | ||
178 | } | ||
179 | |||
180 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
181 | } | ||
182 | |||
183 | addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) { | ||
184 | const accountName = comment.Account.getDisplayName() | ||
185 | const video = comment.Video | ||
186 | const commentUrl = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() | ||
187 | |||
188 | const text = `Hi dear user,\n\n` + | ||
189 | `A new comment has been posted by ${accountName} on your video ${video.name}` + | ||
190 | `\n\n` + | ||
191 | `You can view it on ${commentUrl} ` + | ||
192 | `\n\n` + | ||
193 | `Cheers,\n` + | ||
194 | `PeerTube.` | ||
195 | |||
196 | const emailPayload: EmailPayload = { | ||
197 | to, | ||
198 | subject: 'New comment on your video ' + video.name, | ||
103 | text | 199 | text |
104 | } | 200 | } |
105 | 201 | ||
106 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 202 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
107 | } | 203 | } |
108 | 204 | ||
109 | async addVideoAbuseReportJob (videoId: number) { | 205 | addNewCommentMentionNotification (to: string[], comment: VideoCommentModel) { |
110 | const video = await VideoModel.load(videoId) | 206 | const accountName = comment.Account.getDisplayName() |
111 | if (!video) throw new Error('Unknown Video id during Abuse report.') | 207 | const video = comment.Video |
208 | const commentUrl = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() | ||
209 | |||
210 | const text = `Hi dear user,\n\n` + | ||
211 | `${accountName} mentioned you on video ${video.name}` + | ||
212 | `\n\n` + | ||
213 | `You can view the comment on ${commentUrl} ` + | ||
214 | `\n\n` + | ||
215 | `Cheers,\n` + | ||
216 | `PeerTube.` | ||
217 | |||
218 | const emailPayload: EmailPayload = { | ||
219 | to, | ||
220 | subject: 'Mention on video ' + video.name, | ||
221 | text | ||
222 | } | ||
223 | |||
224 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
225 | } | ||
226 | |||
227 | addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) { | ||
228 | const videoUrl = CONFIG.WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() | ||
112 | 229 | ||
113 | const text = `Hi,\n\n` + | 230 | const text = `Hi,\n\n` + |
114 | `Your instance received an abuse for the following video ${video.url}\n\n` + | 231 | `${CONFIG.WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` + |
115 | `Cheers,\n` + | 232 | `Cheers,\n` + |
116 | `PeerTube.` | 233 | `PeerTube.` |
117 | 234 | ||
118 | const to = await UserModel.listEmailsWithRight(UserRight.MANAGE_VIDEO_ABUSES) | ||
119 | const emailPayload: EmailPayload = { | 235 | const emailPayload: EmailPayload = { |
120 | to, | 236 | to, |
121 | subject: '[PeerTube] Received a video abuse', | 237 | subject: '[PeerTube] Received a video abuse', |
@@ -125,16 +241,27 @@ class Emailer { | |||
125 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 241 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
126 | } | 242 | } |
127 | 243 | ||
128 | async addVideoBlacklistReportJob (videoId: number, reason?: string) { | 244 | addNewUserRegistrationNotification (to: string[], user: UserModel) { |
129 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | 245 | const text = `Hi,\n\n` + |
130 | if (!video) throw new Error('Unknown Video id during Blacklist report.') | 246 | `User ${user.username} just registered on ${CONFIG.WEBSERVER.HOST} PeerTube instance.\n\n` + |
131 | // It's not our user | 247 | `Cheers,\n` + |
132 | if (video.remote === true) return | 248 | `PeerTube.` |
249 | |||
250 | const emailPayload: EmailPayload = { | ||
251 | to, | ||
252 | subject: '[PeerTube] New user registration on ' + CONFIG.WEBSERVER.HOST, | ||
253 | text | ||
254 | } | ||
255 | |||
256 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
257 | } | ||
133 | 258 | ||
134 | const user = await UserModel.loadById(video.VideoChannel.Account.userId) | 259 | addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) { |
260 | const videoName = videoBlacklist.Video.name | ||
261 | const videoUrl = CONFIG.WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() | ||
135 | 262 | ||
136 | const reasonString = reason ? ` for the following reason: ${reason}` : '' | 263 | const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' |
137 | const blockedString = `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been blacklisted${reasonString}.` | 264 | const blockedString = `Your video ${videoName} (${videoUrl} on ${CONFIG.WEBSERVER.HOST} has been blacklisted${reasonString}.` |
138 | 265 | ||
139 | const text = 'Hi,\n\n' + | 266 | const text = 'Hi,\n\n' + |
140 | blockedString + | 267 | blockedString + |
@@ -142,33 +269,26 @@ class Emailer { | |||
142 | 'Cheers,\n' + | 269 | 'Cheers,\n' + |
143 | `PeerTube.` | 270 | `PeerTube.` |
144 | 271 | ||
145 | const to = user.email | ||
146 | const emailPayload: EmailPayload = { | 272 | const emailPayload: EmailPayload = { |
147 | to: [ to ], | 273 | to, |
148 | subject: `[PeerTube] Video ${video.name} blacklisted`, | 274 | subject: `[PeerTube] Video ${videoName} blacklisted`, |
149 | text | 275 | text |
150 | } | 276 | } |
151 | 277 | ||
152 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 278 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
153 | } | 279 | } |
154 | 280 | ||
155 | async addVideoUnblacklistReportJob (videoId: number) { | 281 | addVideoUnblacklistNotification (to: string[], video: VideoModel) { |
156 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | 282 | const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() |
157 | if (!video) throw new Error('Unknown Video id during Blacklist report.') | ||
158 | // It's not our user | ||
159 | if (video.remote === true) return | ||
160 | |||
161 | const user = await UserModel.loadById(video.VideoChannel.Account.userId) | ||
162 | 283 | ||
163 | const text = 'Hi,\n\n' + | 284 | const text = 'Hi,\n\n' + |
164 | `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been unblacklisted.` + | 285 | `Your video ${video.name} (${videoUrl}) on ${CONFIG.WEBSERVER.HOST} has been unblacklisted.` + |
165 | '\n\n' + | 286 | '\n\n' + |
166 | 'Cheers,\n' + | 287 | 'Cheers,\n' + |
167 | `PeerTube.` | 288 | `PeerTube.` |
168 | 289 | ||
169 | const to = user.email | ||
170 | const emailPayload: EmailPayload = { | 290 | const emailPayload: EmailPayload = { |
171 | to: [ to ], | 291 | to, |
172 | subject: `[PeerTube] Video ${video.name} unblacklisted`, | 292 | subject: `[PeerTube] Video ${video.name} unblacklisted`, |
173 | text | 293 | text |
174 | } | 294 | } |
@@ -176,6 +296,40 @@ class Emailer { | |||
176 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 296 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
177 | } | 297 | } |
178 | 298 | ||
299 | addPasswordResetEmailJob (to: string, resetPasswordUrl: string) { | ||
300 | const text = `Hi dear user,\n\n` + | ||
301 | `A reset password procedure for your account ${to} has been requested on ${CONFIG.WEBSERVER.HOST} ` + | ||
302 | `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + | ||
303 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | ||
304 | `Cheers,\n` + | ||
305 | `PeerTube.` | ||
306 | |||
307 | const emailPayload: EmailPayload = { | ||
308 | to: [ to ], | ||
309 | subject: 'Reset your PeerTube password', | ||
310 | text | ||
311 | } | ||
312 | |||
313 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
314 | } | ||
315 | |||
316 | addVerifyEmailJob (to: string, verifyEmailUrl: string) { | ||
317 | const text = `Welcome to PeerTube,\n\n` + | ||
318 | `To start using PeerTube on ${CONFIG.WEBSERVER.HOST} you must verify your email! ` + | ||
319 | `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` + | ||
320 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | ||
321 | `Cheers,\n` + | ||
322 | `PeerTube.` | ||
323 | |||
324 | const emailPayload: EmailPayload = { | ||
325 | to: [ to ], | ||
326 | subject: 'Verify your PeerTube email', | ||
327 | text | ||
328 | } | ||
329 | |||
330 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
331 | } | ||
332 | |||
179 | addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) { | 333 | addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) { |
180 | const reasonString = reason ? ` for the following reason: ${reason}` : '' | 334 | const reasonString = reason ? ` for the following reason: ${reason}` : '' |
181 | const blockedWord = blocked ? 'blocked' : 'unblocked' | 335 | const blockedWord = blocked ? 'blocked' : 'unblocked' |
@@ -197,13 +351,32 @@ class Emailer { | |||
197 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 351 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
198 | } | 352 | } |
199 | 353 | ||
200 | sendMail (to: string[], subject: string, text: string) { | 354 | addContactFormJob (fromEmail: string, fromName: string, body: string) { |
201 | if (!this.transporter) { | 355 | const text = 'Hello dear admin,\n\n' + |
356 | fromName + ' sent you a message' + | ||
357 | '\n\n---------------------------------------\n\n' + | ||
358 | body + | ||
359 | '\n\n---------------------------------------\n\n' + | ||
360 | 'Cheers,\n' + | ||
361 | 'PeerTube.' | ||
362 | |||
363 | const emailPayload: EmailPayload = { | ||
364 | from: fromEmail, | ||
365 | to: [ CONFIG.ADMIN.EMAIL ], | ||
366 | subject: '[PeerTube] Contact form submitted', | ||
367 | text | ||
368 | } | ||
369 | |||
370 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
371 | } | ||
372 | |||
373 | sendMail (to: string[], subject: string, text: string, from?: string) { | ||
374 | if (!Emailer.isEnabled()) { | ||
202 | throw new Error('Cannot send mail because SMTP is not configured.') | 375 | throw new Error('Cannot send mail because SMTP is not configured.') |
203 | } | 376 | } |
204 | 377 | ||
205 | return this.transporter.sendMail({ | 378 | return this.transporter.sendMail({ |
206 | from: CONFIG.SMTP.FROM_ADDRESS, | 379 | from: from || CONFIG.SMTP.FROM_ADDRESS, |
207 | to: to.join(','), | 380 | to: to.join(','), |
208 | subject, | 381 | subject, |
209 | text | 382 | text |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts new file mode 100644 index 000000000..3575981f4 --- /dev/null +++ b/server/lib/hls.ts | |||
@@ -0,0 +1,164 @@ | |||
1 | import { VideoModel } from '../models/video/video' | ||
2 | import { basename, join, dirname } from 'path' | ||
3 | import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers' | ||
4 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' | ||
5 | import { getVideoFileSize } from '../helpers/ffmpeg-utils' | ||
6 | import { sha256 } from '../helpers/core-utils' | ||
7 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
8 | import { logger } from '../helpers/logger' | ||
9 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' | ||
10 | import { generateRandomString } from '../helpers/utils' | ||
11 | import { flatten, uniq } from 'lodash' | ||
12 | |||
13 | async function updateMasterHLSPlaylist (video: VideoModel) { | ||
14 | const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | ||
15 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] | ||
16 | const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | ||
17 | |||
18 | for (const file of video.VideoFiles) { | ||
19 | // If we did not generated a playlist for this resolution, skip | ||
20 | const filePlaylistPath = join(directory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
21 | if (await pathExists(filePlaylistPath) === false) continue | ||
22 | |||
23 | const videoFilePath = video.getVideoFilePath(file) | ||
24 | |||
25 | const size = await getVideoFileSize(videoFilePath) | ||
26 | |||
27 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) | ||
28 | const resolution = `RESOLUTION=${size.width}x${size.height}` | ||
29 | |||
30 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` | ||
31 | if (file.fps) line += ',FRAME-RATE=' + file.fps | ||
32 | |||
33 | masterPlaylists.push(line) | ||
34 | masterPlaylists.push(VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
35 | } | ||
36 | |||
37 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') | ||
38 | } | ||
39 | |||
40 | async function updateSha256Segments (video: VideoModel) { | ||
41 | const json: { [filename: string]: { [range: string]: string } } = {} | ||
42 | |||
43 | const playlistDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | ||
44 | |||
45 | // For all the resolutions available for this video | ||
46 | for (const file of video.VideoFiles) { | ||
47 | const rangeHashes: { [range: string]: string } = {} | ||
48 | |||
49 | const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)) | ||
50 | const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
51 | |||
52 | // Maybe the playlist is not generated for this resolution yet | ||
53 | if (!await pathExists(playlistPath)) continue | ||
54 | |||
55 | const playlistContent = await readFile(playlistPath) | ||
56 | const ranges = getRangesFromPlaylist(playlistContent.toString()) | ||
57 | |||
58 | const fd = await open(videoPath, 'r') | ||
59 | for (const range of ranges) { | ||
60 | const buf = Buffer.alloc(range.length) | ||
61 | await read(fd, buf, 0, range.length, range.offset) | ||
62 | |||
63 | rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf) | ||
64 | } | ||
65 | await close(fd) | ||
66 | |||
67 | const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution) | ||
68 | json[videoFilename] = rangeHashes | ||
69 | } | ||
70 | |||
71 | const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | ||
72 | await outputJSON(outputPath, json) | ||
73 | } | ||
74 | |||
75 | function getRangesFromPlaylist (playlistContent: string) { | ||
76 | const ranges: { offset: number, length: number }[] = [] | ||
77 | const lines = playlistContent.split('\n') | ||
78 | const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/ | ||
79 | |||
80 | for (const line of lines) { | ||
81 | const captured = regex.exec(line) | ||
82 | |||
83 | if (captured) { | ||
84 | ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) }) | ||
85 | } | ||
86 | } | ||
87 | |||
88 | return ranges | ||
89 | } | ||
90 | |||
91 | function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) { | ||
92 | let timer | ||
93 | |||
94 | logger.info('Importing HLS playlist %s', playlistUrl) | ||
95 | |||
96 | return new Promise<string>(async (res, rej) => { | ||
97 | const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10)) | ||
98 | |||
99 | await ensureDir(tmpDirectory) | ||
100 | |||
101 | timer = setTimeout(() => { | ||
102 | deleteTmpDirectory(tmpDirectory) | ||
103 | |||
104 | return rej(new Error('HLS download timeout.')) | ||
105 | }, timeout) | ||
106 | |||
107 | try { | ||
108 | // Fetch master playlist | ||
109 | const subPlaylistUrls = await fetchUniqUrls(playlistUrl) | ||
110 | |||
111 | const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u)) | ||
112 | const fileUrls = uniq(flatten(await Promise.all(subRequests))) | ||
113 | |||
114 | logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls }) | ||
115 | |||
116 | for (const fileUrl of fileUrls) { | ||
117 | const destPath = join(tmpDirectory, basename(fileUrl)) | ||
118 | |||
119 | await doRequestAndSaveToFile({ uri: fileUrl }, destPath) | ||
120 | } | ||
121 | |||
122 | clearTimeout(timer) | ||
123 | |||
124 | await move(tmpDirectory, destinationDir, { overwrite: true }) | ||
125 | |||
126 | return res() | ||
127 | } catch (err) { | ||
128 | deleteTmpDirectory(tmpDirectory) | ||
129 | |||
130 | return rej(err) | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | function deleteTmpDirectory (directory: string) { | ||
135 | remove(directory) | ||
136 | .catch(err => logger.error('Cannot delete path on HLS download error.', { err })) | ||
137 | } | ||
138 | |||
139 | async function fetchUniqUrls (playlistUrl: string) { | ||
140 | const { body } = await doRequest<string>({ uri: playlistUrl }) | ||
141 | |||
142 | if (!body) return [] | ||
143 | |||
144 | const urls = body.split('\n') | ||
145 | .filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4')) | ||
146 | .map(url => { | ||
147 | if (url.startsWith('http://') || url.startsWith('https://')) return url | ||
148 | |||
149 | return `${dirname(playlistUrl)}/${url}` | ||
150 | }) | ||
151 | |||
152 | return uniq(urls) | ||
153 | } | ||
154 | } | ||
155 | |||
156 | // --------------------------------------------------------------------------- | ||
157 | |||
158 | export { | ||
159 | updateMasterHLSPlaylist, | ||
160 | updateSha256Segments, | ||
161 | downloadPlaylistSegments | ||
162 | } | ||
163 | |||
164 | // --------------------------------------------------------------------------- | ||
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts index 36d0f237b..b4d381062 100644 --- a/server/lib/job-queue/handlers/activitypub-follow.ts +++ b/server/lib/job-queue/handlers/activitypub-follow.ts | |||
@@ -8,6 +8,7 @@ import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor' | |||
8 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 8 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
9 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 9 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
10 | import { ActorModel } from '../../../models/activitypub/actor' | 10 | import { ActorModel } from '../../../models/activitypub/actor' |
11 | import { Notifier } from '../../notifier' | ||
11 | 12 | ||
12 | export type ActivitypubFollowPayload = { | 13 | export type ActivitypubFollowPayload = { |
13 | followerActorId: number | 14 | followerActorId: number |
@@ -42,7 +43,7 @@ export { | |||
42 | 43 | ||
43 | // --------------------------------------------------------------------------- | 44 | // --------------------------------------------------------------------------- |
44 | 45 | ||
45 | function follow (fromActor: ActorModel, targetActor: ActorModel) { | 46 | async function follow (fromActor: ActorModel, targetActor: ActorModel) { |
46 | if (fromActor.id === targetActor.id) { | 47 | if (fromActor.id === targetActor.id) { |
47 | throw new Error('Follower is the same than target actor.') | 48 | throw new Error('Follower is the same than target actor.') |
48 | } | 49 | } |
@@ -50,7 +51,7 @@ function follow (fromActor: ActorModel, targetActor: ActorModel) { | |||
50 | // Same server, direct accept | 51 | // Same server, direct accept |
51 | const state = !fromActor.serverId && !targetActor.serverId ? 'accepted' : 'pending' | 52 | const state = !fromActor.serverId && !targetActor.serverId ? 'accepted' : 'pending' |
52 | 53 | ||
53 | return sequelizeTypescript.transaction(async t => { | 54 | const actorFollow = await sequelizeTypescript.transaction(async t => { |
54 | const [ actorFollow ] = await ActorFollowModel.findOrCreate({ | 55 | const [ actorFollow ] = await ActorFollowModel.findOrCreate({ |
55 | where: { | 56 | where: { |
56 | actorId: fromActor.id, | 57 | actorId: fromActor.id, |
@@ -68,5 +69,9 @@ function follow (fromActor: ActorModel, targetActor: ActorModel) { | |||
68 | 69 | ||
69 | // Send a notification to remote server if our follow is not already accepted | 70 | // Send a notification to remote server if our follow is not already accepted |
70 | if (actorFollow.state !== 'accepted') await sendFollow(actorFollow) | 71 | if (actorFollow.state !== 'accepted') await sendFollow(actorFollow) |
72 | |||
73 | return actorFollow | ||
71 | }) | 74 | }) |
75 | |||
76 | if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewFollow(actorFollow) | ||
72 | } | 77 | } |
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts index 03a9e12a4..9493945ff 100644 --- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts | |||
@@ -3,8 +3,9 @@ import * as Bluebird from 'bluebird' | |||
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { doRequest } from '../../../helpers/requests' | 4 | import { doRequest } from '../../../helpers/requests' |
5 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 5 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
6 | import { buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' | 6 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' |
7 | import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers' | 7 | import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers' |
8 | import { ActorFollowScoreCache } from '../../cache' | ||
8 | 9 | ||
9 | export type ActivitypubHttpBroadcastPayload = { | 10 | export type ActivitypubHttpBroadcastPayload = { |
10 | uris: string[] | 11 | uris: string[] |
@@ -25,7 +26,8 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) { | |||
25 | uri: '', | 26 | uri: '', |
26 | json: body, | 27 | json: body, |
27 | httpSignature: httpSignatureOptions, | 28 | httpSignature: httpSignatureOptions, |
28 | timeout: JOB_REQUEST_TIMEOUT | 29 | timeout: JOB_REQUEST_TIMEOUT, |
30 | headers: buildGlobalHeaders(body) | ||
29 | } | 31 | } |
30 | 32 | ||
31 | const badUrls: string[] = [] | 33 | const badUrls: string[] = [] |
@@ -37,7 +39,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) { | |||
37 | .catch(() => badUrls.push(uri)) | 39 | .catch(() => badUrls.push(uri)) |
38 | }, { concurrency: BROADCAST_CONCURRENCY }) | 40 | }, { concurrency: BROADCAST_CONCURRENCY }) |
39 | 41 | ||
40 | return ActorFollowModel.updateActorFollowsScore(goodUrls, badUrls, undefined) | 42 | return ActorFollowScoreCache.Instance.updateActorFollowsScore(goodUrls, badUrls) |
41 | } | 43 | } |
42 | 44 | ||
43 | // --------------------------------------------------------------------------- | 45 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts index 42217c27c..67ccfa995 100644 --- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts +++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts | |||
@@ -23,7 +23,7 @@ async function processActivityPubHttpFetcher (job: Bull.Job) { | |||
23 | if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) | 23 | if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) |
24 | 24 | ||
25 | const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { | 25 | const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { |
26 | 'activity': items => processActivities(items), | 26 | 'activity': items => processActivities(items, { outboxUrl: payload.uri }), |
27 | 'video-likes': items => createRates(items, video, 'like'), | 27 | 'video-likes': items => createRates(items, video, 'like'), |
28 | 'video-dislikes': items => createRates(items, video, 'dislike'), | 28 | 'video-dislikes': items => createRates(items, video, 'dislike'), |
29 | 'video-shares': items => addVideoShares(items, video), | 29 | 'video-shares': items => addVideoShares(items, video), |
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts index c90d735f6..3973dcdc8 100644 --- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts | |||
@@ -1,9 +1,9 @@ | |||
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 { doRequest } from '../../../helpers/requests' | 3 | import { doRequest } from '../../../helpers/requests' |
4 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 4 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' |
5 | import { buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' | ||
6 | import { JOB_REQUEST_TIMEOUT } from '../../../initializers' | 5 | import { JOB_REQUEST_TIMEOUT } from '../../../initializers' |
6 | import { ActorFollowScoreCache } from '../../cache' | ||
7 | 7 | ||
8 | export type ActivitypubHttpUnicastPayload = { | 8 | export type ActivitypubHttpUnicastPayload = { |
9 | uri: string | 9 | uri: string |
@@ -25,14 +25,15 @@ async function processActivityPubHttpUnicast (job: Bull.Job) { | |||
25 | uri, | 25 | uri, |
26 | json: body, | 26 | json: body, |
27 | httpSignature: httpSignatureOptions, | 27 | httpSignature: httpSignatureOptions, |
28 | timeout: JOB_REQUEST_TIMEOUT | 28 | timeout: JOB_REQUEST_TIMEOUT, |
29 | headers: buildGlobalHeaders(body) | ||
29 | } | 30 | } |
30 | 31 | ||
31 | try { | 32 | try { |
32 | await doRequest(options) | 33 | await doRequest(options) |
33 | ActorFollowModel.updateActorFollowsScore([ uri ], [], undefined) | 34 | ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) |
34 | } catch (err) { | 35 | } catch (err) { |
35 | ActorFollowModel.updateActorFollowsScore([], [ uri ], undefined) | 36 | ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) |
36 | 37 | ||
37 | throw err | 38 | throw err |
38 | } | 39 | } |
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts new file mode 100644 index 000000000..454b975fe --- /dev/null +++ b/server/lib/job-queue/handlers/activitypub-refresher.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | import * as Bull from 'bull' | ||
2 | import { logger } from '../../../helpers/logger' | ||
3 | import { fetchVideoByUrl } from '../../../helpers/video' | ||
4 | import { refreshVideoIfNeeded, refreshActorIfNeeded } from '../../activitypub' | ||
5 | import { ActorModel } from '../../../models/activitypub/actor' | ||
6 | |||
7 | export type RefreshPayload = { | ||
8 | type: 'video' | 'actor' | ||
9 | url: string | ||
10 | } | ||
11 | |||
12 | async function refreshAPObject (job: Bull.Job) { | ||
13 | const payload = job.data as RefreshPayload | ||
14 | |||
15 | logger.info('Processing AP refresher in job %d for %s.', job.id, payload.url) | ||
16 | |||
17 | if (payload.type === 'video') return refreshVideo(payload.url) | ||
18 | if (payload.type === 'actor') return refreshActor(payload.url) | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | export { | ||
24 | refreshActor, | ||
25 | refreshAPObject | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | async function refreshVideo (videoUrl: string) { | ||
31 | const fetchType = 'all' as 'all' | ||
32 | const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true } | ||
33 | |||
34 | const videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) | ||
35 | if (videoFromDatabase) { | ||
36 | const refreshOptions = { | ||
37 | video: videoFromDatabase, | ||
38 | fetchedType: fetchType, | ||
39 | syncParam | ||
40 | } | ||
41 | |||
42 | await refreshVideoIfNeeded(refreshOptions) | ||
43 | } | ||
44 | } | ||
45 | |||
46 | async function refreshActor (actorUrl: string) { | ||
47 | const fetchType = 'all' as 'all' | ||
48 | const actor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorUrl) | ||
49 | |||
50 | if (actor) { | ||
51 | await refreshActorIfNeeded(actor, fetchType) | ||
52 | } | ||
53 | |||
54 | } | ||
diff --git a/server/lib/job-queue/handlers/email.ts b/server/lib/job-queue/handlers/email.ts index 73d98ae54..220d0af32 100644 --- a/server/lib/job-queue/handlers/email.ts +++ b/server/lib/job-queue/handlers/email.ts | |||
@@ -6,13 +6,14 @@ export type EmailPayload = { | |||
6 | to: string[] | 6 | to: string[] |
7 | subject: string | 7 | subject: string |
8 | text: string | 8 | text: string |
9 | from?: string | ||
9 | } | 10 | } |
10 | 11 | ||
11 | async function processEmail (job: Bull.Job) { | 12 | async function processEmail (job: Bull.Job) { |
12 | const payload = job.data as EmailPayload | 13 | const payload = job.data as EmailPayload |
13 | logger.info('Processing email in job %d.', job.id) | 14 | logger.info('Processing email in job %d.', job.id) |
14 | 15 | ||
15 | return Emailer.Instance.sendMail(payload.to, payload.subject, payload.text) | 16 | return Emailer.Instance.sendMail(payload.to, payload.subject, payload.text, payload.from) |
16 | } | 17 | } |
17 | 18 | ||
18 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts index 36092665e..4961d4502 100644 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts | |||
@@ -1,8 +1,12 @@ | |||
1 | import { buildSignedActivity } from '../../../../helpers/activitypub' | 1 | import { buildSignedActivity } from '../../../../helpers/activitypub' |
2 | import { getServerActor } from '../../../../helpers/utils' | 2 | import { getServerActor } from '../../../../helpers/utils' |
3 | import { ActorModel } from '../../../../models/activitypub/actor' | 3 | import { ActorModel } from '../../../../models/activitypub/actor' |
4 | import { sha256 } from '../../../../helpers/core-utils' | ||
5 | import { HTTP_SIGNATURE } from '../../../../initializers' | ||
4 | 6 | ||
5 | async function computeBody (payload: { body: any, signatureActorId?: number }) { | 7 | type Payload = { body: any, signatureActorId?: number } |
8 | |||
9 | async function computeBody (payload: Payload) { | ||
6 | let body = payload.body | 10 | let body = payload.body |
7 | 11 | ||
8 | if (payload.signatureActorId) { | 12 | if (payload.signatureActorId) { |
@@ -14,7 +18,7 @@ async function computeBody (payload: { body: any, signatureActorId?: number }) { | |||
14 | return body | 18 | return body |
15 | } | 19 | } |
16 | 20 | ||
17 | async function buildSignedRequestOptions (payload: { signatureActorId?: number }) { | 21 | async function buildSignedRequestOptions (payload: Payload) { |
18 | let actor: ActorModel | null | 22 | let actor: ActorModel | null |
19 | if (payload.signatureActorId) { | 23 | if (payload.signatureActorId) { |
20 | actor = await ActorModel.load(payload.signatureActorId) | 24 | actor = await ActorModel.load(payload.signatureActorId) |
@@ -26,14 +30,29 @@ async function buildSignedRequestOptions (payload: { signatureActorId?: number } | |||
26 | 30 | ||
27 | const keyId = actor.getWebfingerUrl() | 31 | const keyId = actor.getWebfingerUrl() |
28 | return { | 32 | return { |
29 | algorithm: 'rsa-sha256', | 33 | algorithm: HTTP_SIGNATURE.ALGORITHM, |
30 | authorizationHeaderName: 'Signature', | 34 | authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, |
31 | keyId, | 35 | keyId, |
32 | key: actor.privateKey | 36 | key: actor.privateKey, |
37 | headers: HTTP_SIGNATURE.HEADERS_TO_SIGN | ||
38 | } | ||
39 | } | ||
40 | |||
41 | function buildGlobalHeaders (body: any) { | ||
42 | return { | ||
43 | 'Digest': buildDigest(body) | ||
33 | } | 44 | } |
34 | } | 45 | } |
35 | 46 | ||
47 | function buildDigest (body: any) { | ||
48 | const rawBody = typeof body === 'string' ? body : JSON.stringify(body) | ||
49 | |||
50 | return 'SHA-256=' + sha256(rawBody, 'base64') | ||
51 | } | ||
52 | |||
36 | export { | 53 | export { |
54 | buildDigest, | ||
55 | buildGlobalHeaders, | ||
37 | computeBody, | 56 | computeBody, |
38 | buildSignedRequestOptions | 57 | buildSignedRequestOptions |
39 | } | 58 | } |
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts index 1463c93fc..04983155c 100644 --- a/server/lib/job-queue/handlers/video-file.ts +++ b/server/lib/job-queue/handlers/video-file.ts | |||
@@ -5,16 +5,18 @@ import { VideoModel } from '../../../models/video/video' | |||
5 | import { JobQueue } from '../job-queue' | 5 | import { JobQueue } from '../job-queue' |
6 | import { federateVideoIfNeeded } from '../../activitypub' | 6 | import { federateVideoIfNeeded } from '../../activitypub' |
7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript, CONFIG } from '../../../initializers' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding' | 11 | import { generateHlsPlaylist, importVideoFile, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding' |
12 | import { Notifier } from '../../notifier' | ||
12 | 13 | ||
13 | export type VideoFilePayload = { | 14 | export type VideoFilePayload = { |
14 | videoUUID: string | 15 | videoUUID: string |
15 | isNewVideo?: boolean | ||
16 | resolution?: VideoResolution | 16 | resolution?: VideoResolution |
17 | isNewVideo?: boolean | ||
17 | isPortraitMode?: boolean | 18 | isPortraitMode?: boolean |
19 | generateHlsPlaylist?: boolean | ||
18 | } | 20 | } |
19 | 21 | ||
20 | export type VideoFileImportPayload = { | 22 | export type VideoFileImportPayload = { |
@@ -50,34 +52,51 @@ async function processVideoFile (job: Bull.Job) { | |||
50 | return undefined | 52 | return undefined |
51 | } | 53 | } |
52 | 54 | ||
53 | // Transcoding in other resolution | 55 | if (payload.generateHlsPlaylist) { |
54 | if (payload.resolution) { | 56 | await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false) |
57 | |||
58 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) | ||
59 | } else if (payload.resolution) { // Transcoding in other resolution | ||
55 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) | 60 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) |
56 | 61 | ||
57 | await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) | 62 | await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video, payload) |
58 | } else { | 63 | } else { |
59 | await optimizeOriginalVideofile(video) | 64 | await optimizeVideofile(video) |
60 | 65 | ||
61 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) | 66 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload) |
62 | } | 67 | } |
63 | 68 | ||
64 | return video | 69 | return video |
65 | } | 70 | } |
66 | 71 | ||
67 | async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { | 72 | async function onHlsPlaylistGenerationSuccess (video: VideoModel) { |
73 | if (video === undefined) return undefined | ||
74 | |||
75 | await sequelizeTypescript.transaction(async t => { | ||
76 | // Maybe the video changed in database, refresh it | ||
77 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | ||
78 | // Video does not exist anymore | ||
79 | if (!videoDatabase) return undefined | ||
80 | |||
81 | // If the video was not published, we consider it is a new one for other instances | ||
82 | await federateVideoIfNeeded(videoDatabase, false, t) | ||
83 | }) | ||
84 | } | ||
85 | |||
86 | async function onVideoFileTranscoderOrImportSuccess (video: VideoModel, payload?: VideoFilePayload) { | ||
68 | if (video === undefined) return undefined | 87 | if (video === undefined) return undefined |
69 | 88 | ||
70 | return sequelizeTypescript.transaction(async t => { | 89 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { |
71 | // Maybe the video changed in database, refresh it | 90 | // Maybe the video changed in database, refresh it |
72 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | 91 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) |
73 | // Video does not exist anymore | 92 | // Video does not exist anymore |
74 | if (!videoDatabase) return undefined | 93 | if (!videoDatabase) return undefined |
75 | 94 | ||
76 | let isNewVideo = false | 95 | let videoPublished = false |
77 | 96 | ||
78 | // We transcoded the video file in another format, now we can publish it | 97 | // We transcoded the video file in another format, now we can publish it |
79 | if (videoDatabase.state !== VideoState.PUBLISHED) { | 98 | if (videoDatabase.state !== VideoState.PUBLISHED) { |
80 | isNewVideo = true | 99 | videoPublished = true |
81 | 100 | ||
82 | videoDatabase.state = VideoState.PUBLISHED | 101 | videoDatabase.state = VideoState.PUBLISHED |
83 | videoDatabase.publishedAt = new Date() | 102 | videoDatabase.publishedAt = new Date() |
@@ -85,21 +104,29 @@ async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { | |||
85 | } | 104 | } |
86 | 105 | ||
87 | // If the video was not published, we consider it is a new one for other instances | 106 | // If the video was not published, we consider it is a new one for other instances |
88 | await federateVideoIfNeeded(videoDatabase, isNewVideo, t) | 107 | await federateVideoIfNeeded(videoDatabase, videoPublished, t) |
89 | 108 | ||
90 | return undefined | 109 | return { videoDatabase, videoPublished } |
91 | }) | 110 | }) |
111 | |||
112 | // don't notify prior to scheduled video update | ||
113 | if (videoPublished && !videoDatabase.ScheduleVideoUpdate) { | ||
114 | Notifier.Instance.notifyOnNewVideo(videoDatabase) | ||
115 | Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) | ||
116 | } | ||
117 | |||
118 | await createHlsJobIfEnabled(payload) | ||
92 | } | 119 | } |
93 | 120 | ||
94 | async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) { | 121 | async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: VideoFilePayload) { |
95 | if (video === undefined) return undefined | 122 | if (videoArg === undefined) return undefined |
96 | 123 | ||
97 | // Outside the transaction (IO on disk) | 124 | // Outside the transaction (IO on disk) |
98 | const { videoFileResolution } = await video.getOriginalFileResolution() | 125 | const { videoFileResolution } = await videoArg.getOriginalFileResolution() |
99 | 126 | ||
100 | return sequelizeTypescript.transaction(async t => { | 127 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { |
101 | // Maybe the video changed in database, refresh it | 128 | // Maybe the video changed in database, refresh it |
102 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | 129 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t) |
103 | // Video does not exist anymore | 130 | // Video does not exist anymore |
104 | if (!videoDatabase) return undefined | 131 | if (!videoDatabase) return undefined |
105 | 132 | ||
@@ -110,8 +137,10 @@ async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boole | |||
110 | { resolutions: resolutionsEnabled } | 137 | { resolutions: resolutionsEnabled } |
111 | ) | 138 | ) |
112 | 139 | ||
140 | let videoPublished = false | ||
141 | |||
113 | if (resolutionsEnabled.length !== 0) { | 142 | if (resolutionsEnabled.length !== 0) { |
114 | const tasks: Bluebird<any>[] = [] | 143 | const tasks: Bluebird<Bull.Job<any>>[] = [] |
115 | 144 | ||
116 | for (const resolution of resolutionsEnabled) { | 145 | for (const resolution of resolutionsEnabled) { |
117 | const dataInput = { | 146 | const dataInput = { |
@@ -127,15 +156,27 @@ async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boole | |||
127 | 156 | ||
128 | logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) | 157 | logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) |
129 | } else { | 158 | } else { |
159 | videoPublished = true | ||
160 | |||
130 | // No transcoding to do, it's now published | 161 | // No transcoding to do, it's now published |
131 | video.state = VideoState.PUBLISHED | 162 | videoDatabase.state = VideoState.PUBLISHED |
132 | video = await video.save({ transaction: t }) | 163 | videoDatabase = await videoDatabase.save({ transaction: t }) |
133 | 164 | ||
134 | logger.info('No transcoding jobs created for video %s (no resolutions).', video.uuid) | 165 | logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy }) |
135 | } | 166 | } |
136 | 167 | ||
137 | return federateVideoIfNeeded(video, isNewVideo, t) | 168 | await federateVideoIfNeeded(videoDatabase, payload.isNewVideo, t) |
169 | |||
170 | return { videoDatabase, videoPublished } | ||
138 | }) | 171 | }) |
172 | |||
173 | // don't notify prior to scheduled video update | ||
174 | if (!videoDatabase.ScheduleVideoUpdate) { | ||
175 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) | ||
176 | if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) | ||
177 | } | ||
178 | |||
179 | await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })) | ||
139 | } | 180 | } |
140 | 181 | ||
141 | // --------------------------------------------------------------------------- | 182 | // --------------------------------------------------------------------------- |
@@ -144,3 +185,20 @@ export { | |||
144 | processVideoFile, | 185 | processVideoFile, |
145 | processVideoFileImport | 186 | processVideoFileImport |
146 | } | 187 | } |
188 | |||
189 | // --------------------------------------------------------------------------- | ||
190 | |||
191 | function createHlsJobIfEnabled (payload?: VideoFilePayload) { | ||
192 | // Generate HLS playlist? | ||
193 | if (payload && CONFIG.TRANSCODING.HLS.ENABLED) { | ||
194 | const hlsTranscodingPayload = { | ||
195 | videoUUID: payload.videoUUID, | ||
196 | resolution: payload.resolution, | ||
197 | isPortraitMode: payload.isPortraitMode, | ||
198 | |||
199 | generateHlsPlaylist: true | ||
200 | } | ||
201 | |||
202 | return JobQueue.Instance.createJob({ type: 'video-file', payload: hlsTranscodingPayload }) | ||
203 | } | ||
204 | } | ||
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index e3f2a276c..12004dcd7 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -6,15 +6,16 @@ import { VideoImportState } from '../../../../shared/models/videos' | |||
6 | import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 6 | import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
7 | import { extname, join } from 'path' | 7 | import { extname, join } from 'path' |
8 | import { VideoFileModel } from '../../../models/video/video-file' | 8 | import { VideoFileModel } from '../../../models/video/video-file' |
9 | import { CONFIG, sequelizeTypescript, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' | 9 | import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' |
10 | import { doRequestAndSaveToFile } from '../../../helpers/requests' | 10 | import { downloadImage } from '../../../helpers/requests' |
11 | import { VideoState } from '../../../../shared' | 11 | import { VideoState } from '../../../../shared' |
12 | import { JobQueue } from '../index' | 12 | import { JobQueue } from '../index' |
13 | import { federateVideoIfNeeded } from '../../activitypub' | 13 | import { federateVideoIfNeeded } from '../../activitypub' |
14 | import { VideoModel } from '../../../models/video/video' | 14 | import { VideoModel } from '../../../models/video/video' |
15 | import { downloadWebTorrentVideo } from '../../../helpers/webtorrent' | 15 | import { downloadWebTorrentVideo } from '../../../helpers/webtorrent' |
16 | import { getSecureTorrentName } from '../../../helpers/utils' | 16 | import { getSecureTorrentName } from '../../../helpers/utils' |
17 | import { remove, rename, stat } from 'fs-extra' | 17 | import { remove, move, stat } from 'fs-extra' |
18 | import { Notifier } from '../../notifier' | ||
18 | 19 | ||
19 | type VideoImportYoutubeDLPayload = { | 20 | type VideoImportYoutubeDLPayload = { |
20 | type: 'youtube-dl' | 21 | type: 'youtube-dl' |
@@ -109,6 +110,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
109 | let tempVideoPath: string | 110 | let tempVideoPath: string |
110 | let videoDestFile: string | 111 | let videoDestFile: string |
111 | let videoFile: VideoFileModel | 112 | let videoFile: VideoFileModel |
113 | |||
112 | try { | 114 | try { |
113 | // Download video from youtubeDL | 115 | // Download video from youtubeDL |
114 | tempVideoPath = await downloader() | 116 | tempVideoPath = await downloader() |
@@ -133,19 +135,18 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
133 | videoId: videoImport.videoId | 135 | videoId: videoImport.videoId |
134 | } | 136 | } |
135 | videoFile = new VideoFileModel(videoFileData) | 137 | videoFile = new VideoFileModel(videoFileData) |
136 | // Import if the import fails, to clean files | 138 | // To clean files if the import fails |
137 | videoImport.Video.VideoFiles = [ videoFile ] | 139 | videoImport.Video.VideoFiles = [ videoFile ] |
138 | 140 | ||
139 | // Move file | 141 | // Move file |
140 | videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImport.Video.getVideoFilename(videoFile)) | 142 | videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImport.Video.getVideoFilename(videoFile)) |
141 | await rename(tempVideoPath, videoDestFile) | 143 | await move(tempVideoPath, videoDestFile) |
142 | tempVideoPath = null // This path is not used anymore | 144 | tempVideoPath = null // This path is not used anymore |
143 | 145 | ||
144 | // Process thumbnail | 146 | // Process thumbnail |
145 | if (options.downloadThumbnail) { | 147 | if (options.downloadThumbnail) { |
146 | if (options.thumbnailUrl) { | 148 | if (options.thumbnailUrl) { |
147 | const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName()) | 149 | await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE) |
148 | await doRequestAndSaveToFile({ method: 'GET', uri: options.thumbnailUrl }, destThumbnailPath) | ||
149 | } else { | 150 | } else { |
150 | await videoImport.Video.createThumbnail(videoFile) | 151 | await videoImport.Video.createThumbnail(videoFile) |
151 | } | 152 | } |
@@ -156,8 +157,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
156 | // Process preview | 157 | // Process preview |
157 | if (options.downloadPreview) { | 158 | if (options.downloadPreview) { |
158 | if (options.thumbnailUrl) { | 159 | if (options.thumbnailUrl) { |
159 | const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName()) | 160 | await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE) |
160 | await doRequestAndSaveToFile({ method: 'GET', uri: options.thumbnailUrl }, destPreviewPath) | ||
161 | } else { | 161 | } else { |
162 | await videoImport.Video.createPreview(videoFile) | 162 | await videoImport.Video.createPreview(videoFile) |
163 | } | 163 | } |
@@ -180,7 +180,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
180 | // Update video DB object | 180 | // Update video DB object |
181 | video.duration = duration | 181 | video.duration = duration |
182 | video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED | 182 | video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED |
183 | const videoUpdated = await video.save({ transaction: t }) | 183 | await video.save({ transaction: t }) |
184 | 184 | ||
185 | // Now we can federate the video (reload from database, we need more attributes) | 185 | // Now we can federate the video (reload from database, we need more attributes) |
186 | const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | 186 | const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) |
@@ -192,10 +192,13 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
192 | 192 | ||
193 | logger.info('Video %s imported.', video.uuid) | 193 | logger.info('Video %s imported.', video.uuid) |
194 | 194 | ||
195 | videoImportUpdated.Video = videoUpdated | 195 | videoImportUpdated.Video = videoForFederation |
196 | return videoImportUpdated | 196 | return videoImportUpdated |
197 | }) | 197 | }) |
198 | 198 | ||
199 | Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video) | ||
200 | Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) | ||
201 | |||
199 | // Create transcoding jobs? | 202 | // Create transcoding jobs? |
200 | if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { | 203 | if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { |
201 | // Put uuid because we don't have id auto incremented for now | 204 | // Put uuid because we don't have id auto incremented for now |
@@ -218,6 +221,8 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
218 | videoImport.state = VideoImportState.FAILED | 221 | videoImport.state = VideoImportState.FAILED |
219 | await videoImport.save() | 222 | await videoImport.save() |
220 | 223 | ||
224 | Notifier.Instance.notifyOnFinishedVideoImport(videoImport, false) | ||
225 | |||
221 | throw err | 226 | throw err |
222 | } | 227 | } |
223 | } | 228 | } |
diff --git a/server/lib/job-queue/handlers/video-views.ts b/server/lib/job-queue/handlers/video-views.ts index cf180a11a..fa1fd13b3 100644 --- a/server/lib/job-queue/handlers/video-views.ts +++ b/server/lib/job-queue/handlers/video-views.ts | |||
@@ -3,8 +3,9 @@ import { logger } from '../../../helpers/logger' | |||
3 | import { VideoModel } from '../../../models/video/video' | 3 | import { VideoModel } from '../../../models/video/video' |
4 | import { VideoViewModel } from '../../../models/video/video-views' | 4 | import { VideoViewModel } from '../../../models/video/video-views' |
5 | import { isTestInstance } from '../../../helpers/core-utils' | 5 | import { isTestInstance } from '../../../helpers/core-utils' |
6 | import { federateVideoIfNeeded } from '../../activitypub' | ||
6 | 7 | ||
7 | async function processVideosViewsViews () { | 8 | async function processVideosViews () { |
8 | const lastHour = new Date() | 9 | const lastHour = new Date() |
9 | 10 | ||
10 | // In test mode, we run this function multiple times per hour, so we don't want the values of the previous hour | 11 | // In test mode, we run this function multiple times per hour, so we don't want the values of the previous hour |
@@ -22,13 +23,9 @@ async function processVideosViewsViews () { | |||
22 | for (const videoId of videoIds) { | 23 | for (const videoId of videoIds) { |
23 | try { | 24 | try { |
24 | const views = await Redis.Instance.getVideoViews(videoId, hour) | 25 | const views = await Redis.Instance.getVideoViews(videoId, hour) |
25 | if (isNaN(views)) { | 26 | if (views) { |
26 | logger.error('Cannot process videos views of video %d in hour %d: views number is NaN.', videoId, hour) | ||
27 | } else { | ||
28 | logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) | 27 | logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) |
29 | 28 | ||
30 | await VideoModel.incrementViews(videoId, views) | ||
31 | |||
32 | try { | 29 | try { |
33 | await VideoViewModel.create({ | 30 | await VideoViewModel.create({ |
34 | startDate, | 31 | startDate, |
@@ -36,6 +33,16 @@ async function processVideosViewsViews () { | |||
36 | views, | 33 | views, |
37 | videoId | 34 | videoId |
38 | }) | 35 | }) |
36 | |||
37 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
38 | if (video.isOwned()) { | ||
39 | // If this is a remote video, the origin instance will send us an update | ||
40 | await VideoModel.incrementViews(videoId, views) | ||
41 | |||
42 | // Send video update | ||
43 | video.views += views | ||
44 | await federateVideoIfNeeded(video, false) | ||
45 | } | ||
39 | } catch (err) { | 46 | } catch (err) { |
40 | logger.debug('Cannot create video views for video %d in hour %d. Maybe the video does not exist anymore?', videoId, hour) | 47 | logger.debug('Cannot create video views for video %d in hour %d. Maybe the video does not exist anymore?', videoId, hour) |
41 | } | 48 | } |
@@ -51,5 +58,5 @@ async function processVideosViewsViews () { | |||
51 | // --------------------------------------------------------------------------- | 58 | // --------------------------------------------------------------------------- |
52 | 59 | ||
53 | export { | 60 | export { |
54 | processVideosViewsViews | 61 | processVideosViews |
55 | } | 62 | } |
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 0696ba43c..ba9cbe0d9 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -10,7 +10,8 @@ import { EmailPayload, processEmail } from './handlers/email' | |||
10 | import { processVideoFile, processVideoFileImport, VideoFileImportPayload, VideoFilePayload } from './handlers/video-file' | 10 | import { processVideoFile, processVideoFileImport, VideoFileImportPayload, VideoFilePayload } from './handlers/video-file' |
11 | import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' | 11 | import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' |
12 | import { processVideoImport, VideoImportPayload } from './handlers/video-import' | 12 | import { processVideoImport, VideoImportPayload } from './handlers/video-import' |
13 | import { processVideosViewsViews } from './handlers/video-views' | 13 | import { processVideosViews } from './handlers/video-views' |
14 | import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher' | ||
14 | 15 | ||
15 | type CreateJobArgument = | 16 | type CreateJobArgument = |
16 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | | 17 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | |
@@ -21,6 +22,7 @@ type CreateJobArgument = | |||
21 | { type: 'video-file', payload: VideoFilePayload } | | 22 | { type: 'video-file', payload: VideoFilePayload } | |
22 | { type: 'email', payload: EmailPayload } | | 23 | { type: 'email', payload: EmailPayload } | |
23 | { type: 'video-import', payload: VideoImportPayload } | | 24 | { type: 'video-import', payload: VideoImportPayload } | |
25 | { type: 'activitypub-refresher', payload: RefreshPayload } | | ||
24 | { type: 'videos-views', payload: {} } | 26 | { type: 'videos-views', payload: {} } |
25 | 27 | ||
26 | const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = { | 28 | const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = { |
@@ -32,7 +34,8 @@ const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = { | |||
32 | 'video-file': processVideoFile, | 34 | 'video-file': processVideoFile, |
33 | 'email': processEmail, | 35 | 'email': processEmail, |
34 | 'video-import': processVideoImport, | 36 | 'video-import': processVideoImport, |
35 | 'videos-views': processVideosViewsViews | 37 | 'videos-views': processVideosViews, |
38 | 'activitypub-refresher': refreshAPObject | ||
36 | } | 39 | } |
37 | 40 | ||
38 | const jobTypes: JobType[] = [ | 41 | const jobTypes: JobType[] = [ |
@@ -44,7 +47,8 @@ const jobTypes: JobType[] = [ | |||
44 | 'video-file', | 47 | 'video-file', |
45 | 'video-file-import', | 48 | 'video-file-import', |
46 | 'video-import', | 49 | 'video-import', |
47 | 'videos-views' | 50 | 'videos-views', |
51 | 'activitypub-refresher' | ||
48 | ] | 52 | ] |
49 | 53 | ||
50 | class JobQueue { | 54 | class JobQueue { |
@@ -84,7 +88,6 @@ class JobQueue { | |||
84 | 88 | ||
85 | queue.on('error', err => { | 89 | queue.on('error', err => { |
86 | logger.error('Error in job queue %s.', handlerName, { err }) | 90 | logger.error('Error in job queue %s.', handlerName, { err }) |
87 | process.exit(-1) | ||
88 | }) | 91 | }) |
89 | 92 | ||
90 | this.queues[handlerName] = queue | 93 | this.queues[handlerName] = queue |
@@ -162,10 +165,10 @@ class JobQueue { | |||
162 | return total | 165 | return total |
163 | } | 166 | } |
164 | 167 | ||
165 | removeOldJobs () { | 168 | async removeOldJobs () { |
166 | for (const key of Object.keys(this.queues)) { | 169 | for (const key of Object.keys(this.queues)) { |
167 | const queue = this.queues[key] | 170 | const queue = this.queues[key] |
168 | queue.clean(JOB_COMPLETED_LIFETIME, 'completed') | 171 | await queue.clean(JOB_COMPLETED_LIFETIME, 'completed') |
169 | } | 172 | } |
170 | } | 173 | } |
171 | 174 | ||
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts new file mode 100644 index 000000000..d1b331346 --- /dev/null +++ b/server/lib/notifier.ts | |||
@@ -0,0 +1,455 @@ | |||
1 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' | ||
2 | import { logger } from '../helpers/logger' | ||
3 | import { VideoModel } from '../models/video/video' | ||
4 | import { Emailer } from './emailer' | ||
5 | import { UserNotificationModel } from '../models/account/user-notification' | ||
6 | import { VideoCommentModel } from '../models/video/video-comment' | ||
7 | import { UserModel } from '../models/account/user' | ||
8 | import { PeerTubeSocket } from './peertube-socket' | ||
9 | import { CONFIG } from '../initializers/constants' | ||
10 | import { VideoPrivacy, VideoState } from '../../shared/models/videos' | ||
11 | import { VideoAbuseModel } from '../models/video/video-abuse' | ||
12 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | ||
13 | import * as Bluebird from 'bluebird' | ||
14 | import { VideoImportModel } from '../models/video/video-import' | ||
15 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | ||
16 | import { ActorFollowModel } from '../models/activitypub/actor-follow' | ||
17 | import { AccountModel } from '../models/account/account' | ||
18 | |||
19 | class Notifier { | ||
20 | |||
21 | private static instance: Notifier | ||
22 | |||
23 | private constructor () {} | ||
24 | |||
25 | notifyOnNewVideo (video: VideoModel): void { | ||
26 | // Only notify on public and published videos | ||
27 | if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED) return | ||
28 | |||
29 | this.notifySubscribersOfNewVideo(video) | ||
30 | .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) | ||
31 | } | ||
32 | |||
33 | notifyOnPendingVideoPublished (video: VideoModel): void { | ||
34 | // Only notify on public videos that has been published while the user waited transcoding/scheduled update | ||
35 | if (video.waitTranscoding === false && !video.ScheduleVideoUpdate) return | ||
36 | |||
37 | this.notifyOwnedVideoHasBeenPublished(video) | ||
38 | .catch(err => logger.error('Cannot notify owner that its video %s has been published.', video.url, { err })) | ||
39 | } | ||
40 | |||
41 | notifyOnNewComment (comment: VideoCommentModel): void { | ||
42 | this.notifyVideoOwnerOfNewComment(comment) | ||
43 | .catch(err => logger.error('Cannot notify video owner of new comment %s.', comment.url, { err })) | ||
44 | |||
45 | this.notifyOfCommentMention(comment) | ||
46 | .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) | ||
47 | } | ||
48 | |||
49 | notifyOnNewVideoAbuse (videoAbuse: VideoAbuseModel): void { | ||
50 | this.notifyModeratorsOfNewVideoAbuse(videoAbuse) | ||
51 | .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err })) | ||
52 | } | ||
53 | |||
54 | notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void { | ||
55 | this.notifyVideoOwnerOfBlacklist(videoBlacklist) | ||
56 | .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) | ||
57 | } | ||
58 | |||
59 | notifyOnVideoUnblacklist (video: VideoModel): void { | ||
60 | this.notifyVideoOwnerOfUnblacklist(video) | ||
61 | .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', video.url, { err })) | ||
62 | } | ||
63 | |||
64 | notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void { | ||
65 | this.notifyOwnerVideoImportIsFinished(videoImport, success) | ||
66 | .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) | ||
67 | } | ||
68 | |||
69 | notifyOnNewUserRegistration (user: UserModel): void { | ||
70 | this.notifyModeratorsOfNewUserRegistration(user) | ||
71 | .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) | ||
72 | } | ||
73 | |||
74 | notifyOfNewFollow (actorFollow: ActorFollowModel): void { | ||
75 | this.notifyUserOfNewActorFollow(actorFollow) | ||
76 | .catch(err => { | ||
77 | logger.error( | ||
78 | 'Cannot notify owner of channel %s of a new follow by %s.', | ||
79 | actorFollow.ActorFollowing.VideoChannel.getDisplayName(), | ||
80 | actorFollow.ActorFollower.Account.getDisplayName(), | ||
81 | err | ||
82 | ) | ||
83 | }) | ||
84 | } | ||
85 | |||
86 | private async notifySubscribersOfNewVideo (video: VideoModel) { | ||
87 | // List all followers that are users | ||
88 | const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) | ||
89 | |||
90 | logger.info('Notifying %d users of new video %s.', users.length, video.url) | ||
91 | |||
92 | function settingGetter (user: UserModel) { | ||
93 | return user.NotificationSetting.newVideoFromSubscription | ||
94 | } | ||
95 | |||
96 | async function notificationCreator (user: UserModel) { | ||
97 | const notification = await UserNotificationModel.create({ | ||
98 | type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION, | ||
99 | userId: user.id, | ||
100 | videoId: video.id | ||
101 | }) | ||
102 | notification.Video = video | ||
103 | |||
104 | return notification | ||
105 | } | ||
106 | |||
107 | function emailSender (emails: string[]) { | ||
108 | return Emailer.Instance.addNewVideoFromSubscriberNotification(emails, video) | ||
109 | } | ||
110 | |||
111 | return this.notify({ users, settingGetter, notificationCreator, emailSender }) | ||
112 | } | ||
113 | |||
114 | private async notifyVideoOwnerOfNewComment (comment: VideoCommentModel) { | ||
115 | if (comment.Video.isOwned() === false) return | ||
116 | |||
117 | const user = await UserModel.loadByVideoId(comment.videoId) | ||
118 | |||
119 | // Not our user or user comments its own video | ||
120 | if (!user || comment.Account.userId === user.id) return | ||
121 | |||
122 | const accountMuted = await AccountBlocklistModel.isAccountMutedBy(user.Account.id, comment.accountId) | ||
123 | if (accountMuted) return | ||
124 | |||
125 | logger.info('Notifying user %s of new comment %s.', user.username, comment.url) | ||
126 | |||
127 | function settingGetter (user: UserModel) { | ||
128 | return user.NotificationSetting.newCommentOnMyVideo | ||
129 | } | ||
130 | |||
131 | async function notificationCreator (user: UserModel) { | ||
132 | const notification = await UserNotificationModel.create({ | ||
133 | type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, | ||
134 | userId: user.id, | ||
135 | commentId: comment.id | ||
136 | }) | ||
137 | notification.Comment = comment | ||
138 | |||
139 | return notification | ||
140 | } | ||
141 | |||
142 | function emailSender (emails: string[]) { | ||
143 | return Emailer.Instance.addNewCommentOnMyVideoNotification(emails, comment) | ||
144 | } | ||
145 | |||
146 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) | ||
147 | } | ||
148 | |||
149 | private async notifyOfCommentMention (comment: VideoCommentModel) { | ||
150 | const usernames = comment.extractMentions() | ||
151 | let users = await UserModel.listByUsernames(usernames) | ||
152 | |||
153 | if (comment.Video.isOwned()) { | ||
154 | const userException = await UserModel.loadByVideoId(comment.videoId) | ||
155 | users = users.filter(u => u.id !== userException.id) | ||
156 | } | ||
157 | |||
158 | // Don't notify if I mentioned myself | ||
159 | users = users.filter(u => u.Account.id !== comment.accountId) | ||
160 | |||
161 | if (users.length === 0) return | ||
162 | |||
163 | const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(users.map(u => u.Account.id), comment.accountId) | ||
164 | |||
165 | logger.info('Notifying %d users of new comment %s.', users.length, comment.url) | ||
166 | |||
167 | function settingGetter (user: UserModel) { | ||
168 | if (accountMutedHash[user.Account.id] === true) return UserNotificationSettingValue.NONE | ||
169 | |||
170 | return user.NotificationSetting.commentMention | ||
171 | } | ||
172 | |||
173 | async function notificationCreator (user: UserModel) { | ||
174 | const notification = await UserNotificationModel.create({ | ||
175 | type: UserNotificationType.COMMENT_MENTION, | ||
176 | userId: user.id, | ||
177 | commentId: comment.id | ||
178 | }) | ||
179 | notification.Comment = comment | ||
180 | |||
181 | return notification | ||
182 | } | ||
183 | |||
184 | function emailSender (emails: string[]) { | ||
185 | return Emailer.Instance.addNewCommentMentionNotification(emails, comment) | ||
186 | } | ||
187 | |||
188 | return this.notify({ users, settingGetter, notificationCreator, emailSender }) | ||
189 | } | ||
190 | |||
191 | private async notifyUserOfNewActorFollow (actorFollow: ActorFollowModel) { | ||
192 | if (actorFollow.ActorFollowing.isOwned() === false) return | ||
193 | |||
194 | // Account follows one of our account? | ||
195 | let followType: 'account' | 'channel' = 'channel' | ||
196 | let user = await UserModel.loadByChannelActorId(actorFollow.ActorFollowing.id) | ||
197 | |||
198 | // Account follows one of our channel? | ||
199 | if (!user) { | ||
200 | user = await UserModel.loadByAccountActorId(actorFollow.ActorFollowing.id) | ||
201 | followType = 'account' | ||
202 | } | ||
203 | |||
204 | if (!user) return | ||
205 | |||
206 | if (!actorFollow.ActorFollower.Account || !actorFollow.ActorFollower.Account.name) { | ||
207 | actorFollow.ActorFollower.Account = await actorFollow.ActorFollower.$get('Account') as AccountModel | ||
208 | } | ||
209 | const followerAccount = actorFollow.ActorFollower.Account | ||
210 | |||
211 | const accountMuted = await AccountBlocklistModel.isAccountMutedBy(user.Account.id, followerAccount.id) | ||
212 | if (accountMuted) return | ||
213 | |||
214 | logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName()) | ||
215 | |||
216 | function settingGetter (user: UserModel) { | ||
217 | return user.NotificationSetting.newFollow | ||
218 | } | ||
219 | |||
220 | async function notificationCreator (user: UserModel) { | ||
221 | const notification = await UserNotificationModel.create({ | ||
222 | type: UserNotificationType.NEW_FOLLOW, | ||
223 | userId: user.id, | ||
224 | actorFollowId: actorFollow.id | ||
225 | }) | ||
226 | notification.ActorFollow = actorFollow | ||
227 | |||
228 | return notification | ||
229 | } | ||
230 | |||
231 | function emailSender (emails: string[]) { | ||
232 | return Emailer.Instance.addNewFollowNotification(emails, actorFollow, followType) | ||
233 | } | ||
234 | |||
235 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) | ||
236 | } | ||
237 | |||
238 | private async notifyModeratorsOfNewVideoAbuse (videoAbuse: VideoAbuseModel) { | ||
239 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) | ||
240 | if (moderators.length === 0) return | ||
241 | |||
242 | logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url) | ||
243 | |||
244 | function settingGetter (user: UserModel) { | ||
245 | return user.NotificationSetting.videoAbuseAsModerator | ||
246 | } | ||
247 | |||
248 | async function notificationCreator (user: UserModel) { | ||
249 | const notification = await UserNotificationModel.create({ | ||
250 | type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, | ||
251 | userId: user.id, | ||
252 | videoAbuseId: videoAbuse.id | ||
253 | }) | ||
254 | notification.VideoAbuse = videoAbuse | ||
255 | |||
256 | return notification | ||
257 | } | ||
258 | |||
259 | function emailSender (emails: string[]) { | ||
260 | return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, videoAbuse) | ||
261 | } | ||
262 | |||
263 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | ||
264 | } | ||
265 | |||
266 | private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) { | ||
267 | const user = await UserModel.loadByVideoId(videoBlacklist.videoId) | ||
268 | if (!user) return | ||
269 | |||
270 | logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url) | ||
271 | |||
272 | function settingGetter (user: UserModel) { | ||
273 | return user.NotificationSetting.blacklistOnMyVideo | ||
274 | } | ||
275 | |||
276 | async function notificationCreator (user: UserModel) { | ||
277 | const notification = await UserNotificationModel.create({ | ||
278 | type: UserNotificationType.BLACKLIST_ON_MY_VIDEO, | ||
279 | userId: user.id, | ||
280 | videoBlacklistId: videoBlacklist.id | ||
281 | }) | ||
282 | notification.VideoBlacklist = videoBlacklist | ||
283 | |||
284 | return notification | ||
285 | } | ||
286 | |||
287 | function emailSender (emails: string[]) { | ||
288 | return Emailer.Instance.addVideoBlacklistNotification(emails, videoBlacklist) | ||
289 | } | ||
290 | |||
291 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) | ||
292 | } | ||
293 | |||
294 | private async notifyVideoOwnerOfUnblacklist (video: VideoModel) { | ||
295 | const user = await UserModel.loadByVideoId(video.id) | ||
296 | if (!user) return | ||
297 | |||
298 | logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url) | ||
299 | |||
300 | function settingGetter (user: UserModel) { | ||
301 | return user.NotificationSetting.blacklistOnMyVideo | ||
302 | } | ||
303 | |||
304 | async function notificationCreator (user: UserModel) { | ||
305 | const notification = await UserNotificationModel.create({ | ||
306 | type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO, | ||
307 | userId: user.id, | ||
308 | videoId: video.id | ||
309 | }) | ||
310 | notification.Video = video | ||
311 | |||
312 | return notification | ||
313 | } | ||
314 | |||
315 | function emailSender (emails: string[]) { | ||
316 | return Emailer.Instance.addVideoUnblacklistNotification(emails, video) | ||
317 | } | ||
318 | |||
319 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) | ||
320 | } | ||
321 | |||
322 | private async notifyOwnedVideoHasBeenPublished (video: VideoModel) { | ||
323 | const user = await UserModel.loadByVideoId(video.id) | ||
324 | if (!user) return | ||
325 | |||
326 | logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url) | ||
327 | |||
328 | function settingGetter (user: UserModel) { | ||
329 | return user.NotificationSetting.myVideoPublished | ||
330 | } | ||
331 | |||
332 | async function notificationCreator (user: UserModel) { | ||
333 | const notification = await UserNotificationModel.create({ | ||
334 | type: UserNotificationType.MY_VIDEO_PUBLISHED, | ||
335 | userId: user.id, | ||
336 | videoId: video.id | ||
337 | }) | ||
338 | notification.Video = video | ||
339 | |||
340 | return notification | ||
341 | } | ||
342 | |||
343 | function emailSender (emails: string[]) { | ||
344 | return Emailer.Instance.myVideoPublishedNotification(emails, video) | ||
345 | } | ||
346 | |||
347 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) | ||
348 | } | ||
349 | |||
350 | private async notifyOwnerVideoImportIsFinished (videoImport: VideoImportModel, success: boolean) { | ||
351 | const user = await UserModel.loadByVideoImportId(videoImport.id) | ||
352 | if (!user) return | ||
353 | |||
354 | logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier()) | ||
355 | |||
356 | function settingGetter (user: UserModel) { | ||
357 | return user.NotificationSetting.myVideoImportFinished | ||
358 | } | ||
359 | |||
360 | async function notificationCreator (user: UserModel) { | ||
361 | const notification = await UserNotificationModel.create({ | ||
362 | type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR, | ||
363 | userId: user.id, | ||
364 | videoImportId: videoImport.id | ||
365 | }) | ||
366 | notification.VideoImport = videoImport | ||
367 | |||
368 | return notification | ||
369 | } | ||
370 | |||
371 | function emailSender (emails: string[]) { | ||
372 | return success | ||
373 | ? Emailer.Instance.myVideoImportSuccessNotification(emails, videoImport) | ||
374 | : Emailer.Instance.myVideoImportErrorNotification(emails, videoImport) | ||
375 | } | ||
376 | |||
377 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) | ||
378 | } | ||
379 | |||
380 | private async notifyModeratorsOfNewUserRegistration (registeredUser: UserModel) { | ||
381 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS) | ||
382 | if (moderators.length === 0) return | ||
383 | |||
384 | logger.info( | ||
385 | 'Notifying %s moderators of new user registration of %s.', | ||
386 | moderators.length, registeredUser.Account.Actor.preferredUsername | ||
387 | ) | ||
388 | |||
389 | function settingGetter (user: UserModel) { | ||
390 | return user.NotificationSetting.newUserRegistration | ||
391 | } | ||
392 | |||
393 | async function notificationCreator (user: UserModel) { | ||
394 | const notification = await UserNotificationModel.create({ | ||
395 | type: UserNotificationType.NEW_USER_REGISTRATION, | ||
396 | userId: user.id, | ||
397 | accountId: registeredUser.Account.id | ||
398 | }) | ||
399 | notification.Account = registeredUser.Account | ||
400 | |||
401 | return notification | ||
402 | } | ||
403 | |||
404 | function emailSender (emails: string[]) { | ||
405 | return Emailer.Instance.addNewUserRegistrationNotification(emails, registeredUser) | ||
406 | } | ||
407 | |||
408 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | ||
409 | } | ||
410 | |||
411 | private async notify (options: { | ||
412 | users: UserModel[], | ||
413 | notificationCreator: (user: UserModel) => Promise<UserNotificationModel>, | ||
414 | emailSender: (emails: string[]) => Promise<any> | Bluebird<any>, | ||
415 | settingGetter: (user: UserModel) => UserNotificationSettingValue | ||
416 | }) { | ||
417 | const emails: string[] = [] | ||
418 | |||
419 | for (const user of options.users) { | ||
420 | if (this.isWebNotificationEnabled(options.settingGetter(user))) { | ||
421 | const notification = await options.notificationCreator(user) | ||
422 | |||
423 | PeerTubeSocket.Instance.sendNotification(user.id, notification) | ||
424 | } | ||
425 | |||
426 | if (this.isEmailEnabled(user, options.settingGetter(user))) { | ||
427 | emails.push(user.email) | ||
428 | } | ||
429 | } | ||
430 | |||
431 | if (emails.length !== 0) { | ||
432 | await options.emailSender(emails) | ||
433 | } | ||
434 | } | ||
435 | |||
436 | private isEmailEnabled (user: UserModel, value: UserNotificationSettingValue) { | ||
437 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified !== true) return false | ||
438 | |||
439 | return value & UserNotificationSettingValue.EMAIL | ||
440 | } | ||
441 | |||
442 | private isWebNotificationEnabled (value: UserNotificationSettingValue) { | ||
443 | return value & UserNotificationSettingValue.WEB | ||
444 | } | ||
445 | |||
446 | static get Instance () { | ||
447 | return this.instance || (this.instance = new this()) | ||
448 | } | ||
449 | } | ||
450 | |||
451 | // --------------------------------------------------------------------------- | ||
452 | |||
453 | export { | ||
454 | Notifier | ||
455 | } | ||
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index 5cbe60b82..2cd2ae97c 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
1 | import { AccessDeniedError } from 'oauth2-server' | 2 | import { AccessDeniedError } from 'oauth2-server' |
2 | import { logger } from '../helpers/logger' | 3 | import { logger } from '../helpers/logger' |
3 | import { UserModel } from '../models/account/user' | 4 | import { UserModel } from '../models/account/user' |
@@ -37,7 +38,7 @@ function clearCacheByToken (token: string) { | |||
37 | function getAccessToken (bearerToken: string) { | 38 | function getAccessToken (bearerToken: string) { |
38 | logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') | 39 | logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') |
39 | 40 | ||
40 | if (accessTokenCache[bearerToken] !== undefined) return accessTokenCache[bearerToken] | 41 | if (accessTokenCache[bearerToken] !== undefined) return Bluebird.resolve(accessTokenCache[bearerToken]) |
41 | 42 | ||
42 | return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) | 43 | return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) |
43 | .then(tokenModel => { | 44 | .then(tokenModel => { |
diff --git a/server/lib/peertube-socket.ts b/server/lib/peertube-socket.ts new file mode 100644 index 000000000..eb84ecd4b --- /dev/null +++ b/server/lib/peertube-socket.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | import * as SocketIO from 'socket.io' | ||
2 | import { authenticateSocket } from '../middlewares' | ||
3 | import { UserNotificationModel } from '../models/account/user-notification' | ||
4 | import { logger } from '../helpers/logger' | ||
5 | import { Server } from 'http' | ||
6 | |||
7 | class PeerTubeSocket { | ||
8 | |||
9 | private static instance: PeerTubeSocket | ||
10 | |||
11 | private userNotificationSockets: { [ userId: number ]: SocketIO.Socket } = {} | ||
12 | |||
13 | private constructor () {} | ||
14 | |||
15 | init (server: Server) { | ||
16 | const io = SocketIO(server) | ||
17 | |||
18 | io.of('/user-notifications') | ||
19 | .use(authenticateSocket) | ||
20 | .on('connection', socket => { | ||
21 | const userId = socket.handshake.query.user.id | ||
22 | |||
23 | logger.debug('User %d connected on the notification system.', userId) | ||
24 | |||
25 | this.userNotificationSockets[userId] = socket | ||
26 | |||
27 | socket.on('disconnect', () => { | ||
28 | logger.debug('User %d disconnected from SocketIO notifications.', userId) | ||
29 | |||
30 | delete this.userNotificationSockets[userId] | ||
31 | }) | ||
32 | }) | ||
33 | } | ||
34 | |||
35 | sendNotification (userId: number, notification: UserNotificationModel) { | ||
36 | const socket = this.userNotificationSockets[userId] | ||
37 | |||
38 | if (!socket) return | ||
39 | |||
40 | socket.emit('new-notification', notification.toFormattedJSON()) | ||
41 | } | ||
42 | |||
43 | static get Instance () { | ||
44 | return this.instance || (this.instance = new this()) | ||
45 | } | ||
46 | } | ||
47 | |||
48 | // --------------------------------------------------------------------------- | ||
49 | |||
50 | export { | ||
51 | PeerTubeSocket | ||
52 | } | ||
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index abd75d512..3628c0583 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -2,7 +2,13 @@ import * as express from 'express' | |||
2 | import { createClient, RedisClient } from 'redis' | 2 | import { createClient, RedisClient } from 'redis' |
3 | import { logger } from '../helpers/logger' | 3 | import { logger } from '../helpers/logger' |
4 | import { generateRandomString } from '../helpers/utils' | 4 | import { generateRandomString } from '../helpers/utils' |
5 | import { CONFIG, USER_PASSWORD_RESET_LIFETIME, USER_EMAIL_VERIFY_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers' | 5 | import { |
6 | CONFIG, | ||
7 | CONTACT_FORM_LIFETIME, | ||
8 | USER_EMAIL_VERIFY_LIFETIME, | ||
9 | USER_PASSWORD_RESET_LIFETIME, | ||
10 | VIDEO_VIEW_LIFETIME | ||
11 | } from '../initializers' | ||
6 | 12 | ||
7 | type CachedRoute = { | 13 | type CachedRoute = { |
8 | body: string, | 14 | body: string, |
@@ -76,6 +82,16 @@ class Redis { | |||
76 | return this.getValue(this.generateVerifyEmailKey(userId)) | 82 | return this.getValue(this.generateVerifyEmailKey(userId)) |
77 | } | 83 | } |
78 | 84 | ||
85 | /************* Contact form per IP *************/ | ||
86 | |||
87 | async setContactFormIp (ip: string) { | ||
88 | return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME) | ||
89 | } | ||
90 | |||
91 | async isContactFormIpExists (ip: string) { | ||
92 | return this.exists(this.generateContactFormKey(ip)) | ||
93 | } | ||
94 | |||
79 | /************* Views per IP *************/ | 95 | /************* Views per IP *************/ |
80 | 96 | ||
81 | setIPVideoView (ip: string, videoUUID: string) { | 97 | setIPVideoView (ip: string, videoUUID: string) { |
@@ -121,7 +137,14 @@ class Redis { | |||
121 | const key = this.generateVideoViewKey(videoId, hour) | 137 | const key = this.generateVideoViewKey(videoId, hour) |
122 | 138 | ||
123 | const valueString = await this.getValue(key) | 139 | const valueString = await this.getValue(key) |
124 | return parseInt(valueString, 10) | 140 | const valueInt = parseInt(valueString, 10) |
141 | |||
142 | if (isNaN(valueInt)) { | ||
143 | logger.error('Cannot get videos views of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString) | ||
144 | return undefined | ||
145 | } | ||
146 | |||
147 | return valueInt | ||
125 | } | 148 | } |
126 | 149 | ||
127 | async getVideosIdViewed (hour: number) { | 150 | async getVideosIdViewed (hour: number) { |
@@ -168,7 +191,11 @@ class Redis { | |||
168 | } | 191 | } |
169 | 192 | ||
170 | private generateViewKey (ip: string, videoUUID: string) { | 193 | private generateViewKey (ip: string, videoUUID: string) { |
171 | return videoUUID + '-' + ip | 194 | return `views-${videoUUID}-${ip}` |
195 | } | ||
196 | |||
197 | private generateContactFormKey (ip: string) { | ||
198 | return 'contact-form-' + ip | ||
172 | } | 199 | } |
173 | 200 | ||
174 | /************* Redis helpers *************/ | 201 | /************* Redis helpers *************/ |
diff --git a/server/lib/schedulers/abstract-scheduler.ts b/server/lib/schedulers/abstract-scheduler.ts index b9d0a4d17..86ea7aa38 100644 --- a/server/lib/schedulers/abstract-scheduler.ts +++ b/server/lib/schedulers/abstract-scheduler.ts | |||
@@ -1,8 +1,11 @@ | |||
1 | import { logger } from '../../helpers/logger' | ||
2 | |||
1 | export abstract class AbstractScheduler { | 3 | export abstract class AbstractScheduler { |
2 | 4 | ||
3 | protected abstract schedulerIntervalMs: number | 5 | protected abstract schedulerIntervalMs: number |
4 | 6 | ||
5 | private interval: NodeJS.Timer | 7 | private interval: NodeJS.Timer |
8 | private isRunning = false | ||
6 | 9 | ||
7 | enable () { | 10 | enable () { |
8 | if (!this.schedulerIntervalMs) throw new Error('Interval is not correctly set.') | 11 | if (!this.schedulerIntervalMs) throw new Error('Interval is not correctly set.') |
@@ -14,5 +17,18 @@ export abstract class AbstractScheduler { | |||
14 | clearInterval(this.interval) | 17 | clearInterval(this.interval) |
15 | } | 18 | } |
16 | 19 | ||
17 | abstract execute () | 20 | async execute () { |
21 | if (this.isRunning === true) return | ||
22 | this.isRunning = true | ||
23 | |||
24 | try { | ||
25 | await this.internalExecute() | ||
26 | } catch (err) { | ||
27 | logger.error('Cannot execute %s scheduler.', this.constructor.name, { err }) | ||
28 | } finally { | ||
29 | this.isRunning = false | ||
30 | } | ||
31 | } | ||
32 | |||
33 | protected abstract internalExecute (): Promise<any> | ||
18 | } | 34 | } |
diff --git a/server/lib/schedulers/bad-actor-follow-scheduler.ts b/server/lib/schedulers/actor-follow-scheduler.ts index 617149aaf..3967be7f8 100644 --- a/server/lib/schedulers/bad-actor-follow-scheduler.ts +++ b/server/lib/schedulers/actor-follow-scheduler.ts | |||
@@ -3,18 +3,35 @@ import { logger } from '../../helpers/logger' | |||
3 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 3 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' |
4 | import { AbstractScheduler } from './abstract-scheduler' | 4 | import { AbstractScheduler } from './abstract-scheduler' |
5 | import { SCHEDULER_INTERVALS_MS } from '../../initializers' | 5 | import { SCHEDULER_INTERVALS_MS } from '../../initializers' |
6 | import { ActorFollowScoreCache } from '../cache' | ||
6 | 7 | ||
7 | export class BadActorFollowScheduler extends AbstractScheduler { | 8 | export class ActorFollowScheduler extends AbstractScheduler { |
8 | 9 | ||
9 | private static instance: AbstractScheduler | 10 | private static instance: AbstractScheduler |
10 | 11 | ||
11 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.badActorFollow | 12 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.actorFollowScores |
12 | 13 | ||
13 | private constructor () { | 14 | private constructor () { |
14 | super() | 15 | super() |
15 | } | 16 | } |
16 | 17 | ||
17 | async execute () { | 18 | protected async internalExecute () { |
19 | await this.processPendingScores() | ||
20 | |||
21 | await this.removeBadActorFollows() | ||
22 | } | ||
23 | |||
24 | private async processPendingScores () { | ||
25 | const pendingScores = ActorFollowScoreCache.Instance.getPendingFollowsScoreCopy() | ||
26 | |||
27 | ActorFollowScoreCache.Instance.clearPendingFollowsScore() | ||
28 | |||
29 | for (const inbox of Object.keys(pendingScores)) { | ||
30 | await ActorFollowModel.updateFollowScore(inbox, pendingScores[inbox]) | ||
31 | } | ||
32 | } | ||
33 | |||
34 | private async removeBadActorFollows () { | ||
18 | if (!isTestInstance()) logger.info('Removing bad actor follows (scheduler).') | 35 | if (!isTestInstance()) logger.info('Removing bad actor follows (scheduler).') |
19 | 36 | ||
20 | try { | 37 | try { |
diff --git a/server/lib/schedulers/remove-old-jobs-scheduler.ts b/server/lib/schedulers/remove-old-jobs-scheduler.ts index a29a6b800..4a4341ba9 100644 --- a/server/lib/schedulers/remove-old-jobs-scheduler.ts +++ b/server/lib/schedulers/remove-old-jobs-scheduler.ts | |||
@@ -14,10 +14,10 @@ export class RemoveOldJobsScheduler extends AbstractScheduler { | |||
14 | super() | 14 | super() |
15 | } | 15 | } |
16 | 16 | ||
17 | async execute () { | 17 | protected internalExecute () { |
18 | if (!isTestInstance()) logger.info('Removing old jobs (scheduler).') | 18 | if (!isTestInstance()) logger.info('Removing old jobs in scheduler.') |
19 | 19 | ||
20 | JobQueue.Instance.removeOldJobs() | 20 | return JobQueue.Instance.removeOldJobs() |
21 | } | 21 | } |
22 | 22 | ||
23 | static get Instance () { | 23 | static get Instance () { |
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts index fd2edfd17..2618a5857 100644 --- a/server/lib/schedulers/update-videos-scheduler.ts +++ b/server/lib/schedulers/update-videos-scheduler.ts | |||
@@ -5,6 +5,8 @@ import { retryTransactionWrapper } from '../../helpers/database-utils' | |||
5 | import { federateVideoIfNeeded } from '../activitypub' | 5 | import { federateVideoIfNeeded } from '../activitypub' |
6 | import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers' | 6 | import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers' |
7 | import { VideoPrivacy } from '../../../shared/models/videos' | 7 | import { VideoPrivacy } from '../../../shared/models/videos' |
8 | import { Notifier } from '../notifier' | ||
9 | import { VideoModel } from '../../models/video/video' | ||
8 | 10 | ||
9 | export class UpdateVideosScheduler extends AbstractScheduler { | 11 | export class UpdateVideosScheduler extends AbstractScheduler { |
10 | 12 | ||
@@ -12,30 +14,20 @@ export class UpdateVideosScheduler extends AbstractScheduler { | |||
12 | 14 | ||
13 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.updateVideos | 15 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.updateVideos |
14 | 16 | ||
15 | private isRunning = false | ||
16 | |||
17 | private constructor () { | 17 | private constructor () { |
18 | super() | 18 | super() |
19 | } | 19 | } |
20 | 20 | ||
21 | async execute () { | 21 | protected async internalExecute () { |
22 | if (this.isRunning === true) return | 22 | return retryTransactionWrapper(this.updateVideos.bind(this)) |
23 | this.isRunning = true | ||
24 | |||
25 | try { | ||
26 | await retryTransactionWrapper(this.updateVideos.bind(this)) | ||
27 | } catch (err) { | ||
28 | logger.error('Cannot execute update videos scheduler.', { err }) | ||
29 | } finally { | ||
30 | this.isRunning = false | ||
31 | } | ||
32 | } | 23 | } |
33 | 24 | ||
34 | private async updateVideos () { | 25 | private async updateVideos () { |
35 | if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined | 26 | if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined |
36 | 27 | ||
37 | return sequelizeTypescript.transaction(async t => { | 28 | const publishedVideos = await sequelizeTypescript.transaction(async t => { |
38 | const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t) | 29 | const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t) |
30 | const publishedVideos: VideoModel[] = [] | ||
39 | 31 | ||
40 | for (const schedule of schedules) { | 32 | for (const schedule of schedules) { |
41 | const video = schedule.Video | 33 | const video = schedule.Video |
@@ -50,11 +42,23 @@ export class UpdateVideosScheduler extends AbstractScheduler { | |||
50 | 42 | ||
51 | await video.save({ transaction: t }) | 43 | await video.save({ transaction: t }) |
52 | await federateVideoIfNeeded(video, isNewVideo, t) | 44 | await federateVideoIfNeeded(video, isNewVideo, t) |
45 | |||
46 | if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) { | ||
47 | video.ScheduleVideoUpdate = schedule | ||
48 | publishedVideos.push(video) | ||
49 | } | ||
53 | } | 50 | } |
54 | 51 | ||
55 | await schedule.destroy({ transaction: t }) | 52 | await schedule.destroy({ transaction: t }) |
56 | } | 53 | } |
54 | |||
55 | return publishedVideos | ||
57 | }) | 56 | }) |
57 | |||
58 | for (const v of publishedVideos) { | ||
59 | Notifier.Instance.notifyOnNewVideo(v) | ||
60 | Notifier.Instance.notifyOnPendingVideoPublished(v) | ||
61 | } | ||
58 | } | 62 | } |
59 | 63 | ||
60 | static get Instance () { | 64 | static get Instance () { |
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index c49a8c89a..1a48f2bd0 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -1,22 +1,31 @@ | |||
1 | import { AbstractScheduler } from './abstract-scheduler' | 1 | import { AbstractScheduler } from './abstract-scheduler' |
2 | import { CONFIG, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers' | 2 | import { CONFIG, HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { VideosRedundancy } from '../../../shared/models/redundancy' | 4 | import { VideosRedundancy } from '../../../shared/models/redundancy' |
5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
6 | import { VideoFileModel } from '../../models/video/video-file' | 6 | import { VideoFileModel } from '../../models/video/video-file' |
7 | import { downloadWebTorrentVideo } from '../../helpers/webtorrent' | 7 | import { downloadWebTorrentVideo } from '../../helpers/webtorrent' |
8 | import { join } from 'path' | 8 | import { join } from 'path' |
9 | import { rename } from 'fs-extra' | 9 | import { move } from 'fs-extra' |
10 | import { getServerActor } from '../../helpers/utils' | 10 | import { getServerActor } from '../../helpers/utils' |
11 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' | 11 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' |
12 | import { getVideoCacheFileActivityPubUrl } from '../activitypub/url' | 12 | import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' |
13 | import { removeVideoRedundancy } from '../redundancy' | 13 | import { removeVideoRedundancy } from '../redundancy' |
14 | import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' | 14 | import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' |
15 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
16 | import { VideoModel } from '../../models/video/video' | ||
17 | import { downloadPlaylistSegments } from '../hls' | ||
18 | |||
19 | type CandidateToDuplicate = { | ||
20 | redundancy: VideosRedundancy, | ||
21 | video: VideoModel, | ||
22 | files: VideoFileModel[], | ||
23 | streamingPlaylists: VideoStreamingPlaylistModel[] | ||
24 | } | ||
15 | 25 | ||
16 | export class VideosRedundancyScheduler extends AbstractScheduler { | 26 | export class VideosRedundancyScheduler extends AbstractScheduler { |
17 | 27 | ||
18 | private static instance: AbstractScheduler | 28 | private static instance: AbstractScheduler |
19 | private executing = false | ||
20 | 29 | ||
21 | protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL | 30 | protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL |
22 | 31 | ||
@@ -24,41 +33,39 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
24 | super() | 33 | super() |
25 | } | 34 | } |
26 | 35 | ||
27 | async execute () { | 36 | protected async internalExecute () { |
28 | if (this.executing) return | 37 | for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { |
29 | 38 | logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy) | |
30 | this.executing = true | ||
31 | |||
32 | for (const obj of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { | ||
33 | logger.info('Running redundancy scheduler for strategy %s.', obj.strategy) | ||
34 | 39 | ||
35 | try { | 40 | try { |
36 | const videoToDuplicate = await this.findVideoToDuplicate(obj) | 41 | const videoToDuplicate = await this.findVideoToDuplicate(redundancyConfig) |
37 | if (!videoToDuplicate) continue | 42 | if (!videoToDuplicate) continue |
38 | 43 | ||
39 | const videoFiles = videoToDuplicate.VideoFiles | 44 | const candidateToDuplicate = { |
40 | videoFiles.forEach(f => f.Video = videoToDuplicate) | 45 | video: videoToDuplicate, |
46 | redundancy: redundancyConfig, | ||
47 | files: videoToDuplicate.VideoFiles, | ||
48 | streamingPlaylists: videoToDuplicate.VideoStreamingPlaylists | ||
49 | } | ||
41 | 50 | ||
42 | await this.purgeCacheIfNeeded(obj, videoFiles) | 51 | await this.purgeCacheIfNeeded(candidateToDuplicate) |
43 | 52 | ||
44 | if (await this.isTooHeavy(obj, videoFiles)) { | 53 | if (await this.isTooHeavy(candidateToDuplicate)) { |
45 | logger.info('Video %s is too big for our cache, skipping.', videoToDuplicate.url) | 54 | logger.info('Video %s is too big for our cache, skipping.', videoToDuplicate.url) |
46 | continue | 55 | continue |
47 | } | 56 | } |
48 | 57 | ||
49 | logger.info('Will duplicate video %s in redundancy scheduler "%s".', videoToDuplicate.url, obj.strategy) | 58 | logger.info('Will duplicate video %s in redundancy scheduler "%s".', videoToDuplicate.url, redundancyConfig.strategy) |
50 | 59 | ||
51 | await this.createVideoRedundancy(obj, videoFiles) | 60 | await this.createVideoRedundancies(candidateToDuplicate) |
52 | } catch (err) { | 61 | } catch (err) { |
53 | logger.error('Cannot run videos redundancy %s.', obj.strategy, { err }) | 62 | logger.error('Cannot run videos redundancy %s.', redundancyConfig.strategy, { err }) |
54 | } | 63 | } |
55 | } | 64 | } |
56 | 65 | ||
57 | await this.extendsLocalExpiration() | 66 | await this.extendsLocalExpiration() |
58 | 67 | ||
59 | await this.purgeRemoteExpired() | 68 | await this.purgeRemoteExpired() |
60 | |||
61 | this.executing = false | ||
62 | } | 69 | } |
63 | 70 | ||
64 | static get Instance () { | 71 | static get Instance () { |
@@ -70,25 +77,35 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
70 | 77 | ||
71 | for (const redundancyModel of expired) { | 78 | for (const redundancyModel of expired) { |
72 | try { | 79 | try { |
73 | await this.extendsOrDeleteRedundancy(redundancyModel) | 80 | const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) |
81 | const candidate = { | ||
82 | redundancy: redundancyConfig, | ||
83 | video: null, | ||
84 | files: [], | ||
85 | streamingPlaylists: [] | ||
86 | } | ||
87 | |||
88 | // If the administrator disabled the redundancy or decreased the cache size, remove this redundancy instead of extending it | ||
89 | if (!redundancyConfig || await this.isTooHeavy(candidate)) { | ||
90 | logger.info('Destroying redundancy %s because the cache size %s is too heavy.', redundancyModel.url, redundancyModel.strategy) | ||
91 | await removeVideoRedundancy(redundancyModel) | ||
92 | } else { | ||
93 | await this.extendsRedundancy(redundancyModel) | ||
94 | } | ||
74 | } catch (err) { | 95 | } catch (err) { |
75 | logger.error('Cannot extend expiration of %s video from our redundancy system.', this.buildEntryLogId(redundancyModel)) | 96 | logger.error( |
97 | 'Cannot extend or remove expiration of %s video from our redundancy system.', this.buildEntryLogId(redundancyModel), | ||
98 | { err } | ||
99 | ) | ||
76 | } | 100 | } |
77 | } | 101 | } |
78 | } | 102 | } |
79 | 103 | ||
80 | private async extendsOrDeleteRedundancy (redundancyModel: VideoRedundancyModel) { | 104 | private async extendsRedundancy (redundancyModel: VideoRedundancyModel) { |
81 | // Refresh the video, maybe it was deleted | ||
82 | const video = await this.loadAndRefreshVideo(redundancyModel.VideoFile.Video.url) | ||
83 | |||
84 | if (!video) { | ||
85 | logger.info('Destroying existing redundancy %s, because the associated video does not exist anymore.', redundancyModel.url) | ||
86 | |||
87 | await redundancyModel.destroy() | ||
88 | return | ||
89 | } | ||
90 | |||
91 | const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) | 105 | const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) |
106 | // Redundancy strategy disabled, remove our redundancy instead of extending expiration | ||
107 | if (!redundancy) await removeVideoRedundancy(redundancyModel) | ||
108 | |||
92 | await this.extendsExpirationOf(redundancyModel, redundancy.minLifetime) | 109 | await this.extendsExpirationOf(redundancyModel, redundancy.minLifetime) |
93 | } | 110 | } |
94 | 111 | ||
@@ -119,49 +136,93 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
119 | } | 136 | } |
120 | } | 137 | } |
121 | 138 | ||
122 | private async createVideoRedundancy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { | 139 | private async createVideoRedundancies (data: CandidateToDuplicate) { |
123 | const serverActor = await getServerActor() | 140 | const video = await this.loadAndRefreshVideo(data.video.url) |
124 | 141 | ||
125 | for (const file of filesToDuplicate) { | 142 | if (!video) { |
126 | const video = await this.loadAndRefreshVideo(file.Video.url) | 143 | logger.info('Video %s we want to duplicate does not existing anymore, skipping.', data.video.url) |
127 | 144 | ||
145 | return | ||
146 | } | ||
147 | |||
148 | for (const file of data.files) { | ||
128 | const existingRedundancy = await VideoRedundancyModel.loadLocalByFileId(file.id) | 149 | const existingRedundancy = await VideoRedundancyModel.loadLocalByFileId(file.id) |
129 | if (existingRedundancy) { | 150 | if (existingRedundancy) { |
130 | await this.extendsOrDeleteRedundancy(existingRedundancy) | 151 | await this.extendsRedundancy(existingRedundancy) |
131 | 152 | ||
132 | continue | 153 | continue |
133 | } | 154 | } |
134 | 155 | ||
135 | if (!video) { | 156 | await this.createVideoFileRedundancy(data.redundancy, video, file) |
136 | logger.info('Video %s we want to duplicate does not existing anymore, skipping.', file.Video.url) | 157 | } |
158 | |||
159 | for (const streamingPlaylist of data.streamingPlaylists) { | ||
160 | const existingRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(streamingPlaylist.id) | ||
161 | if (existingRedundancy) { | ||
162 | await this.extendsRedundancy(existingRedundancy) | ||
137 | 163 | ||
138 | continue | 164 | continue |
139 | } | 165 | } |
140 | 166 | ||
141 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) | 167 | await this.createStreamingPlaylistRedundancy(data.redundancy, video, streamingPlaylist) |
168 | } | ||
169 | } | ||
142 | 170 | ||
143 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 171 | private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: VideoModel, file: VideoFileModel) { |
144 | const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs) | 172 | file.Video = video |
145 | 173 | ||
146 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) | 174 | const serverActor = await getServerActor() |
147 | 175 | ||
148 | const destPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(file)) | 176 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) |
149 | await rename(tmpPath, destPath) | ||
150 | 177 | ||
151 | const createdModel = await VideoRedundancyModel.create({ | 178 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() |
152 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | 179 | const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs) |
153 | url: getVideoCacheFileActivityPubUrl(file), | ||
154 | fileUrl: video.getVideoFileUrl(file, CONFIG.WEBSERVER.URL), | ||
155 | strategy: redundancy.strategy, | ||
156 | videoFileId: file.id, | ||
157 | actorId: serverActor.id | ||
158 | }) | ||
159 | createdModel.VideoFile = file | ||
160 | 180 | ||
161 | await sendCreateCacheFile(serverActor, createdModel) | 181 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) |
162 | 182 | ||
163 | logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url) | 183 | const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file)) |
164 | } | 184 | await move(tmpPath, destPath) |
185 | |||
186 | const createdModel = await VideoRedundancyModel.create({ | ||
187 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | ||
188 | url: getVideoCacheFileActivityPubUrl(file), | ||
189 | fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL), | ||
190 | strategy: redundancy.strategy, | ||
191 | videoFileId: file.id, | ||
192 | actorId: serverActor.id | ||
193 | }) | ||
194 | |||
195 | createdModel.VideoFile = file | ||
196 | |||
197 | await sendCreateCacheFile(serverActor, video, createdModel) | ||
198 | |||
199 | logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url) | ||
200 | } | ||
201 | |||
202 | private async createStreamingPlaylistRedundancy (redundancy: VideosRedundancy, video: VideoModel, playlist: VideoStreamingPlaylistModel) { | ||
203 | playlist.Video = video | ||
204 | |||
205 | const serverActor = await getServerActor() | ||
206 | |||
207 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, redundancy.strategy) | ||
208 | |||
209 | const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) | ||
210 | await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) | ||
211 | |||
212 | const createdModel = await VideoRedundancyModel.create({ | ||
213 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | ||
214 | url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), | ||
215 | fileUrl: playlist.getVideoRedundancyUrl(CONFIG.WEBSERVER.URL), | ||
216 | strategy: redundancy.strategy, | ||
217 | videoStreamingPlaylistId: playlist.id, | ||
218 | actorId: serverActor.id | ||
219 | }) | ||
220 | |||
221 | createdModel.VideoStreamingPlaylist = playlist | ||
222 | |||
223 | await sendCreateCacheFile(serverActor, video, createdModel) | ||
224 | |||
225 | logger.info('Duplicated playlist %s -> %s.', playlist.playlistUrl, createdModel.url) | ||
165 | } | 226 | } |
166 | 227 | ||
167 | private async extendsExpirationOf (redundancy: VideoRedundancyModel, expiresAfterMs: number) { | 228 | private async extendsExpirationOf (redundancy: VideoRedundancyModel, expiresAfterMs: number) { |
@@ -175,8 +236,9 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
175 | await sendUpdateCacheFile(serverActor, redundancy) | 236 | await sendUpdateCacheFile(serverActor, redundancy) |
176 | } | 237 | } |
177 | 238 | ||
178 | private async purgeCacheIfNeeded (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { | 239 | private async purgeCacheIfNeeded (candidateToDuplicate: CandidateToDuplicate) { |
179 | while (this.isTooHeavy(redundancy, filesToDuplicate)) { | 240 | while (this.isTooHeavy(candidateToDuplicate)) { |
241 | const redundancy = candidateToDuplicate.redundancy | ||
180 | const toDelete = await VideoRedundancyModel.loadOldestLocalThatAlreadyExpired(redundancy.strategy, redundancy.minLifetime) | 242 | const toDelete = await VideoRedundancyModel.loadOldestLocalThatAlreadyExpired(redundancy.strategy, redundancy.minLifetime) |
181 | if (!toDelete) return | 243 | if (!toDelete) return |
182 | 244 | ||
@@ -184,12 +246,13 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
184 | } | 246 | } |
185 | } | 247 | } |
186 | 248 | ||
187 | private async isTooHeavy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { | 249 | private async isTooHeavy (candidateToDuplicate: CandidateToDuplicate) { |
188 | const maxSize = redundancy.size - this.getTotalFileSizes(filesToDuplicate) | 250 | const maxSize = candidateToDuplicate.redundancy.size |
189 | 251 | ||
190 | const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(redundancy.strategy) | 252 | const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(candidateToDuplicate.redundancy.strategy) |
253 | const totalWillDuplicate = totalDuplicated + this.getTotalFileSizes(candidateToDuplicate.files, candidateToDuplicate.streamingPlaylists) | ||
191 | 254 | ||
192 | return totalDuplicated > maxSize | 255 | return totalWillDuplicate > maxSize |
193 | } | 256 | } |
194 | 257 | ||
195 | private buildNewExpiration (expiresAfterMs: number) { | 258 | private buildNewExpiration (expiresAfterMs: number) { |
@@ -197,13 +260,15 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
197 | } | 260 | } |
198 | 261 | ||
199 | private buildEntryLogId (object: VideoRedundancyModel) { | 262 | private buildEntryLogId (object: VideoRedundancyModel) { |
200 | return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` | 263 | if (object.VideoFile) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` |
264 | |||
265 | return `${object.VideoStreamingPlaylist.playlistUrl}` | ||
201 | } | 266 | } |
202 | 267 | ||
203 | private getTotalFileSizes (files: VideoFileModel[]) { | 268 | private getTotalFileSizes (files: VideoFileModel[], playlists: VideoStreamingPlaylistModel[]) { |
204 | const fileReducer = (previous: number, current: VideoFileModel) => previous + current.size | 269 | const fileReducer = (previous: number, current: VideoFileModel) => previous + current.size |
205 | 270 | ||
206 | return files.reduce(fileReducer, 0) | 271 | return files.reduce(fileReducer, 0) * playlists.length |
207 | } | 272 | } |
208 | 273 | ||
209 | private async loadAndRefreshVideo (videoUrl: string) { | 274 | private async loadAndRefreshVideo (videoUrl: string) { |
diff --git a/server/lib/schedulers/youtube-dl-update-scheduler.ts b/server/lib/schedulers/youtube-dl-update-scheduler.ts index 461cd045e..aa027116d 100644 --- a/server/lib/schedulers/youtube-dl-update-scheduler.ts +++ b/server/lib/schedulers/youtube-dl-update-scheduler.ts | |||
@@ -12,7 +12,7 @@ export class YoutubeDlUpdateScheduler extends AbstractScheduler { | |||
12 | super() | 12 | super() |
13 | } | 13 | } |
14 | 14 | ||
15 | execute () { | 15 | protected internalExecute () { |
16 | return updateYoutubeDLBinary() | 16 | return updateYoutubeDLBinary() |
17 | } | 17 | } |
18 | 18 | ||
diff --git a/server/lib/user.ts b/server/lib/user.ts index db29469eb..a39ef6c3d 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -9,6 +9,8 @@ import { createVideoChannel } from './video-channel' | |||
9 | import { VideoChannelModel } from '../models/video/video-channel' | 9 | import { VideoChannelModel } from '../models/video/video-channel' |
10 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | 10 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' |
11 | import { ActorModel } from '../models/activitypub/actor' | 11 | import { ActorModel } from '../models/activitypub/actor' |
12 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | ||
13 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' | ||
12 | 14 | ||
13 | async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) { | 15 | async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) { |
14 | const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { | 16 | const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { |
@@ -18,7 +20,9 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse | |||
18 | } | 20 | } |
19 | 21 | ||
20 | const userCreated = await userToCreate.save(userOptions) | 22 | const userCreated = await userToCreate.save(userOptions) |
21 | const accountCreated = await createLocalAccountWithoutKeys(userToCreate.username, userToCreate.id, null, t) | 23 | userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t) |
24 | |||
25 | const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t) | ||
22 | userCreated.Account = accountCreated | 26 | userCreated.Account = accountCreated |
23 | 27 | ||
24 | let channelName = userCreated.username + '_channel' | 28 | let channelName = userCreated.username + '_channel' |
@@ -37,8 +41,13 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse | |||
37 | return { user: userCreated, account: accountCreated, videoChannel } | 41 | return { user: userCreated, account: accountCreated, videoChannel } |
38 | }) | 42 | }) |
39 | 43 | ||
40 | account.Actor = await setAsyncActorKeys(account.Actor) | 44 | const [ accountKeys, channelKeys ] = await Promise.all([ |
41 | videoChannel.Actor = await setAsyncActorKeys(videoChannel.Actor) | 45 | setAsyncActorKeys(account.Actor), |
46 | setAsyncActorKeys(videoChannel.Actor) | ||
47 | ]) | ||
48 | |||
49 | account.Actor = accountKeys | ||
50 | videoChannel.Actor = channelKeys | ||
42 | 51 | ||
43 | return { user, account, videoChannel } as { user: UserModel, account: AccountModel, videoChannel: VideoChannelModel } | 52 | return { user, account, videoChannel } as { user: UserModel, account: AccountModel, videoChannel: VideoChannelModel } |
44 | } | 53 | } |
@@ -83,3 +92,22 @@ export { | |||
83 | createUserAccountAndChannel, | 92 | createUserAccountAndChannel, |
84 | createLocalAccountWithoutKeys | 93 | createLocalAccountWithoutKeys |
85 | } | 94 | } |
95 | |||
96 | // --------------------------------------------------------------------------- | ||
97 | |||
98 | function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Transaction | undefined) { | ||
99 | const values: UserNotificationSetting & { userId: number } = { | ||
100 | userId: user.id, | ||
101 | newVideoFromSubscription: UserNotificationSettingValue.WEB, | ||
102 | newCommentOnMyVideo: UserNotificationSettingValue.WEB, | ||
103 | myVideoImportFinished: UserNotificationSettingValue.WEB, | ||
104 | myVideoPublished: UserNotificationSettingValue.WEB, | ||
105 | videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
106 | blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
107 | newUserRegistration: UserNotificationSettingValue.WEB, | ||
108 | commentMention: UserNotificationSettingValue.WEB, | ||
109 | newFollow: UserNotificationSettingValue.WEB | ||
110 | } | ||
111 | |||
112 | return UserNotificationSettingModel.create(values, { transaction: t }) | ||
113 | } | ||
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts index 70ba7c303..59bce7520 100644 --- a/server/lib/video-comment.ts +++ b/server/lib/video-comment.ts | |||
@@ -64,10 +64,8 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>): | |||
64 | } | 64 | } |
65 | 65 | ||
66 | const parentCommentThread = idx[childComment.inReplyToCommentId] | 66 | const parentCommentThread = idx[childComment.inReplyToCommentId] |
67 | if (!parentCommentThread) { | 67 | // Maybe the parent comment was blocked by the admin/user |
68 | const msg = `Cannot format video thread tree, parent ${childComment.inReplyToCommentId} not found for child ${childComment.id}` | 68 | if (!parentCommentThread) continue |
69 | throw new Error(msg) | ||
70 | } | ||
71 | 69 | ||
72 | parentCommentThread.children.push(childCommentThread) | 70 | parentCommentThread.children.push(childCommentThread) |
73 | idx[childComment.id] = childCommentThread | 71 | idx[childComment.id] = childCommentThread |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index bf3ff78c2..086b860a2 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,22 +1,27 @@ | |||
1 | import { CONFIG } from '../initializers' | 1 | import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers' |
2 | import { join, extname } from 'path' | 2 | import { extname, join } from 'path' |
3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' | 3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' |
4 | import { copy, remove, rename, stat } from 'fs-extra' | 4 | import { copy, ensureDir, move, remove, stat } from 'fs-extra' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { VideoResolution } from '../../shared/models/videos' | 6 | import { VideoResolution } from '../../shared/models/videos' |
7 | import { VideoFileModel } from '../models/video/video-file' | 7 | import { VideoFileModel } from '../models/video/video-file' |
8 | import { VideoModel } from '../models/video/video' | 8 | import { VideoModel } from '../models/video/video' |
9 | import { updateMasterHLSPlaylist, updateSha256Segments } from './hls' | ||
10 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
11 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' | ||
9 | 12 | ||
10 | async function optimizeOriginalVideofile (video: VideoModel) { | 13 | async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { |
11 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 14 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
12 | const newExtname = '.mp4' | 15 | const newExtname = '.mp4' |
13 | const inputVideoFile = video.getOriginalFile() | 16 | |
17 | const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile() | ||
14 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) | 18 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) |
15 | const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) | 19 | const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) |
16 | 20 | ||
17 | const transcodeOptions = { | 21 | const transcodeOptions = { |
18 | inputPath: videoInputPath, | 22 | inputPath: videoInputPath, |
19 | outputPath: videoTranscodedPath | 23 | outputPath: videoTranscodedPath, |
24 | resolution: inputVideoFile.resolution | ||
20 | } | 25 | } |
21 | 26 | ||
22 | // Could be very long! | 27 | // Could be very long! |
@@ -29,7 +34,7 @@ async function optimizeOriginalVideofile (video: VideoModel) { | |||
29 | inputVideoFile.set('extname', newExtname) | 34 | inputVideoFile.set('extname', newExtname) |
30 | 35 | ||
31 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) | 36 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) |
32 | await rename(videoTranscodedPath, videoOutputPath) | 37 | await move(videoTranscodedPath, videoOutputPath) |
33 | const stats = await stat(videoOutputPath) | 38 | const stats = await stat(videoOutputPath) |
34 | const fps = await getVideoFileFPS(videoOutputPath) | 39 | const fps = await getVideoFileFPS(videoOutputPath) |
35 | 40 | ||
@@ -46,7 +51,7 @@ async function optimizeOriginalVideofile (video: VideoModel) { | |||
46 | } | 51 | } |
47 | } | 52 | } |
48 | 53 | ||
49 | async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { | 54 | async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) { |
50 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 55 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
51 | const extname = '.mp4' | 56 | const extname = '.mp4' |
52 | 57 | ||
@@ -59,13 +64,13 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR | |||
59 | size: 0, | 64 | size: 0, |
60 | videoId: video.id | 65 | videoId: video.id |
61 | }) | 66 | }) |
62 | const videoOutputPath = join(videosDirectory, video.getVideoFilename(newVideoFile)) | 67 | const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile)) |
63 | 68 | ||
64 | const transcodeOptions = { | 69 | const transcodeOptions = { |
65 | inputPath: videoInputPath, | 70 | inputPath: videoInputPath, |
66 | outputPath: videoOutputPath, | 71 | outputPath: videoOutputPath, |
67 | resolution, | 72 | resolution, |
68 | isPortraitMode | 73 | isPortraitMode: isPortrait |
69 | } | 74 | } |
70 | 75 | ||
71 | await transcode(transcodeOptions) | 76 | await transcode(transcodeOptions) |
@@ -83,6 +88,41 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR | |||
83 | video.VideoFiles.push(newVideoFile) | 88 | video.VideoFiles.push(newVideoFile) |
84 | } | 89 | } |
85 | 90 | ||
91 | async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { | ||
92 | const baseHlsDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | ||
93 | await ensureDir(join(HLS_PLAYLIST_DIRECTORY, video.uuid)) | ||
94 | |||
95 | const videoInputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(video.getOriginalFile())) | ||
96 | const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | ||
97 | |||
98 | const transcodeOptions = { | ||
99 | inputPath: videoInputPath, | ||
100 | outputPath, | ||
101 | resolution, | ||
102 | isPortraitMode, | ||
103 | |||
104 | hlsPlaylist: { | ||
105 | videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution) | ||
106 | } | ||
107 | } | ||
108 | |||
109 | await transcode(transcodeOptions) | ||
110 | |||
111 | await updateMasterHLSPlaylist(video) | ||
112 | await updateSha256Segments(video) | ||
113 | |||
114 | const playlistUrl = CONFIG.WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | ||
115 | |||
116 | await VideoStreamingPlaylistModel.upsert({ | ||
117 | videoId: video.id, | ||
118 | playlistUrl, | ||
119 | segmentsSha256Url: CONFIG.WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid), | ||
120 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, video.VideoFiles), | ||
121 | |||
122 | type: VideoStreamingPlaylistType.HLS | ||
123 | }) | ||
124 | } | ||
125 | |||
86 | async function importVideoFile (video: VideoModel, inputFilePath: string) { | 126 | async function importVideoFile (video: VideoModel, inputFilePath: string) { |
87 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) | 127 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) |
88 | const { size } = await stat(inputFilePath) | 128 | const { size } = await stat(inputFilePath) |
@@ -124,7 +164,8 @@ async function importVideoFile (video: VideoModel, inputFilePath: string) { | |||
124 | } | 164 | } |
125 | 165 | ||
126 | export { | 166 | export { |
127 | optimizeOriginalVideofile, | 167 | generateHlsPlaylist, |
168 | optimizeVideofile, | ||
128 | transcodeOriginalVideofile, | 169 | transcodeOriginalVideofile, |
129 | importVideoFile | 170 | importVideoFile |
130 | } | 171 | } |
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts index d7f59be8c..01e5dd24e 100644 --- a/server/middlewares/activitypub.ts +++ b/server/middlewares/activitypub.ts | |||
@@ -2,34 +2,32 @@ import { eachSeries } from 'async' | |||
2 | import { NextFunction, Request, RequestHandler, Response } from 'express' | 2 | import { NextFunction, Request, RequestHandler, Response } from 'express' |
3 | import { ActivityPubSignature } from '../../shared' | 3 | import { ActivityPubSignature } from '../../shared' |
4 | import { logger } from '../helpers/logger' | 4 | import { logger } from '../helpers/logger' |
5 | import { isSignatureVerified } from '../helpers/peertube-crypto' | 5 | import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto' |
6 | import { ACCEPT_HEADERS, ACTIVITY_PUB } from '../initializers' | 6 | import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers' |
7 | import { getOrCreateActorAndServerAndModel } from '../lib/activitypub' | 7 | import { getOrCreateActorAndServerAndModel } from '../lib/activitypub' |
8 | import { ActorModel } from '../models/activitypub/actor' | 8 | import { ActorModel } from '../models/activitypub/actor' |
9 | import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger' | ||
9 | 10 | ||
10 | async function checkSignature (req: Request, res: Response, next: NextFunction) { | 11 | async function checkSignature (req: Request, res: Response, next: NextFunction) { |
11 | const signatureObject: ActivityPubSignature = req.body.signature | 12 | try { |
13 | const httpSignatureChecked = await checkHttpSignature(req, res) | ||
14 | if (httpSignatureChecked !== true) return | ||
12 | 15 | ||
13 | const [ creator ] = signatureObject.creator.split('#') | 16 | const actor: ActorModel = res.locals.signature.actor |
14 | 17 | ||
15 | logger.debug('Checking signature of actor %s...', creator) | 18 | // Forwarded activity |
19 | const bodyActor = req.body.actor | ||
20 | const bodyActorId = bodyActor && bodyActor.id ? bodyActor.id : bodyActor | ||
21 | if (bodyActorId && bodyActorId !== actor.url) { | ||
22 | const jsonLDSignatureChecked = await checkJsonLDSignature(req, res) | ||
23 | if (jsonLDSignatureChecked !== true) return | ||
24 | } | ||
16 | 25 | ||
17 | let actor: ActorModel | 26 | return next() |
18 | try { | ||
19 | actor = await getOrCreateActorAndServerAndModel(creator) | ||
20 | } catch (err) { | 27 | } catch (err) { |
21 | logger.warn('Cannot create remote actor %s and check signature.', creator, { err }) | 28 | logger.error('Error in ActivityPub signature checker.', err) |
22 | return res.sendStatus(403) | 29 | return res.sendStatus(403) |
23 | } | 30 | } |
24 | |||
25 | const verified = await isSignatureVerified(actor, req.body) | ||
26 | if (verified === false) return res.sendStatus(403) | ||
27 | |||
28 | res.locals.signature = { | ||
29 | actor | ||
30 | } | ||
31 | |||
32 | return next() | ||
33 | } | 31 | } |
34 | 32 | ||
35 | function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) { | 33 | function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) { |
@@ -55,5 +53,66 @@ function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) { | |||
55 | 53 | ||
56 | export { | 54 | export { |
57 | checkSignature, | 55 | checkSignature, |
58 | executeIfActivityPub | 56 | executeIfActivityPub, |
57 | checkHttpSignature | ||
58 | } | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | async function checkHttpSignature (req: Request, res: Response) { | ||
63 | // FIXME: mastodon does not include the Signature scheme | ||
64 | const sig = req.headers[HTTP_SIGNATURE.HEADER_NAME] as string | ||
65 | if (sig && sig.startsWith('Signature ') === false) req.headers[HTTP_SIGNATURE.HEADER_NAME] = 'Signature ' + sig | ||
66 | |||
67 | const parsed = parseHTTPSignature(req) | ||
68 | |||
69 | const keyId = parsed.keyId | ||
70 | if (!keyId) { | ||
71 | res.sendStatus(403) | ||
72 | return false | ||
73 | } | ||
74 | |||
75 | logger.debug('Checking HTTP signature of actor %s...', keyId) | ||
76 | |||
77 | let [ actorUrl ] = keyId.split('#') | ||
78 | if (actorUrl.startsWith('acct:')) { | ||
79 | actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, '')) | ||
80 | } | ||
81 | |||
82 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | ||
83 | |||
84 | const verified = isHTTPSignatureVerified(parsed, actor) | ||
85 | if (verified !== true) { | ||
86 | res.sendStatus(403) | ||
87 | return false | ||
88 | } | ||
89 | |||
90 | res.locals.signature = { actor } | ||
91 | |||
92 | return true | ||
93 | } | ||
94 | |||
95 | async function checkJsonLDSignature (req: Request, res: Response) { | ||
96 | const signatureObject: ActivityPubSignature = req.body.signature | ||
97 | |||
98 | if (!signatureObject || !signatureObject.creator) { | ||
99 | res.sendStatus(403) | ||
100 | return false | ||
101 | } | ||
102 | |||
103 | const [ creator ] = signatureObject.creator.split('#') | ||
104 | |||
105 | logger.debug('Checking JsonLD signature of actor %s...', creator) | ||
106 | |||
107 | const actor = await getOrCreateActorAndServerAndModel(creator) | ||
108 | const verified = await isJsonLDSignatureVerified(actor, req.body) | ||
109 | |||
110 | if (verified !== true) { | ||
111 | res.sendStatus(403) | ||
112 | return false | ||
113 | } | ||
114 | |||
115 | res.locals.signature = { actor } | ||
116 | |||
117 | return true | ||
59 | } | 118 | } |
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts index 1e00fc731..8ffe75700 100644 --- a/server/middlewares/cache.ts +++ b/server/middlewares/cache.ts | |||
@@ -19,6 +19,7 @@ function cacheRoute (lifetimeArg: string | number) { | |||
19 | logger.debug('No cached results for route %s.', req.originalUrl) | 19 | logger.debug('No cached results for route %s.', req.originalUrl) |
20 | 20 | ||
21 | const sendSave = res.send.bind(res) | 21 | const sendSave = res.send.bind(res) |
22 | const redirectSave = res.redirect.bind(res) | ||
22 | 23 | ||
23 | res.send = (body) => { | 24 | res.send = (body) => { |
24 | if (res.statusCode >= 200 && res.statusCode < 400) { | 25 | if (res.statusCode >= 200 && res.statusCode < 400) { |
@@ -38,6 +39,12 @@ function cacheRoute (lifetimeArg: string | number) { | |||
38 | return sendSave(body) | 39 | return sendSave(body) |
39 | } | 40 | } |
40 | 41 | ||
42 | res.redirect = url => { | ||
43 | done() | ||
44 | |||
45 | return redirectSave(url) | ||
46 | } | ||
47 | |||
41 | return next() | 48 | return next() |
42 | } | 49 | } |
43 | 50 | ||
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts new file mode 100644 index 000000000..5fa9d1ab5 --- /dev/null +++ b/server/middlewares/csp.ts | |||
@@ -0,0 +1,44 @@ | |||
1 | import * as helmet from 'helmet' | ||
2 | import { CONFIG } from '../initializers/constants' | ||
3 | |||
4 | const baseDirectives = Object.assign({}, | ||
5 | { | ||
6 | defaultSrc: ["'none'"], // by default, not specifying default-src = '*' | ||
7 | connectSrc: ['*', 'data:'], | ||
8 | mediaSrc: ["'self'", 'https:', 'blob:'], | ||
9 | fontSrc: ["'self'", 'data:'], | ||
10 | imgSrc: ["'self'", 'data:'], | ||
11 | scriptSrc: ["'self' 'unsafe-inline' 'unsafe-eval'"], | ||
12 | styleSrc: ["'self' 'unsafe-inline'"], | ||
13 | objectSrc: ["'none'"], // only define to allow plugins, else let defaultSrc 'none' block it | ||
14 | formAction: ["'self'"], | ||
15 | frameAncestors: ["'none'"], | ||
16 | baseUri: ["'self'"], | ||
17 | manifestSrc: ["'self'"], | ||
18 | frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed | ||
19 | workerSrc: ["'self'", 'blob:'] // instead of deprecated child-src | ||
20 | }, | ||
21 | CONFIG.SERVICES['CSP-LOGGER'] ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {}, | ||
22 | CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {} | ||
23 | ) | ||
24 | |||
25 | const baseCSP = helmet.contentSecurityPolicy({ | ||
26 | directives: baseDirectives, | ||
27 | browserSniff: false, | ||
28 | reportOnly: true | ||
29 | }) | ||
30 | |||
31 | const embedCSP = helmet.contentSecurityPolicy({ | ||
32 | directives: Object.assign(baseDirectives, { | ||
33 | frameAncestors: ['*'] | ||
34 | }), | ||
35 | browserSniff: false, // assumes a modern browser, but allows CDN in front | ||
36 | reportOnly: true | ||
37 | }) | ||
38 | |||
39 | // --------------------------------------------------------------------------- | ||
40 | |||
41 | export { | ||
42 | baseCSP, | ||
43 | embedCSP | ||
44 | } | ||
diff --git a/server/middlewares/dnt.ts b/server/middlewares/dnt.ts index cabad39c6..607def855 100644 --- a/server/middlewares/dnt.ts +++ b/server/middlewares/dnt.ts | |||
@@ -10,4 +10,4 @@ const advertiseDoNotTrack = (_, res, next) => { | |||
10 | 10 | ||
11 | export { | 11 | export { |
12 | advertiseDoNotTrack | 12 | advertiseDoNotTrack |
13 | } | 13 | } |
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index 0cef26953..b758a8586 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts | |||
@@ -6,3 +6,5 @@ export * from './pagination' | |||
6 | export * from './servers' | 6 | export * from './servers' |
7 | export * from './sort' | 7 | export * from './sort' |
8 | export * from './user-right' | 8 | export * from './user-right' |
9 | export * from './dnt' | ||
10 | export * from './csp' | ||
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts index 5233b66bd..1d193d467 100644 --- a/server/middlewares/oauth.ts +++ b/server/middlewares/oauth.ts | |||
@@ -3,6 +3,8 @@ import * as OAuthServer from 'express-oauth-server' | |||
3 | import 'express-validator' | 3 | import 'express-validator' |
4 | import { OAUTH_LIFETIME } from '../initializers' | 4 | import { OAUTH_LIFETIME } from '../initializers' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { Socket } from 'socket.io' | ||
7 | import { getAccessToken } from '../lib/oauth-model' | ||
6 | 8 | ||
7 | const oAuthServer = new OAuthServer({ | 9 | const oAuthServer = new OAuthServer({ |
8 | useErrorHandler: true, | 10 | useErrorHandler: true, |
@@ -28,9 +30,43 @@ function authenticate (req: express.Request, res: express.Response, next: expres | |||
28 | }) | 30 | }) |
29 | } | 31 | } |
30 | 32 | ||
33 | function authenticateSocket (socket: Socket, next: (err?: any) => void) { | ||
34 | const accessToken = socket.handshake.query.accessToken | ||
35 | |||
36 | logger.debug('Checking socket access token %s.', accessToken) | ||
37 | |||
38 | getAccessToken(accessToken) | ||
39 | .then(tokenDB => { | ||
40 | const now = new Date() | ||
41 | |||
42 | if (!tokenDB || tokenDB.accessTokenExpiresAt < now || tokenDB.refreshTokenExpiresAt < now) { | ||
43 | return next(new Error('Invalid access token.')) | ||
44 | } | ||
45 | |||
46 | socket.handshake.query.user = tokenDB.User | ||
47 | |||
48 | return next() | ||
49 | }) | ||
50 | } | ||
51 | |||
52 | function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) { | ||
53 | return new Promise(resolve => { | ||
54 | // Already authenticated? (or tried to) | ||
55 | if (res.locals.oauth && res.locals.oauth.token.User) return resolve() | ||
56 | |||
57 | if (res.locals.authenticated === false) return res.sendStatus(401) | ||
58 | |||
59 | authenticate(req, res, () => { | ||
60 | return resolve() | ||
61 | }) | ||
62 | }) | ||
63 | } | ||
64 | |||
31 | function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) { | 65 | function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) { |
32 | if (req.header('authorization')) return authenticate(req, res, next) | 66 | if (req.header('authorization')) return authenticate(req, res, next) |
33 | 67 | ||
68 | res.locals.authenticated = false | ||
69 | |||
34 | return next() | 70 | return next() |
35 | } | 71 | } |
36 | 72 | ||
@@ -53,6 +89,8 @@ function token (req: express.Request, res: express.Response, next: express.NextF | |||
53 | 89 | ||
54 | export { | 90 | export { |
55 | authenticate, | 91 | authenticate, |
92 | authenticateSocket, | ||
93 | authenticatePromiseIfNeeded, | ||
56 | optionalAuthenticate, | 94 | optionalAuthenticate, |
57 | token | 95 | token |
58 | } | 96 | } |
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts index 4efe9aafa..be14e92ea 100644 --- a/server/middlewares/validators/activitypub/signature.ts +++ b/server/middlewares/validators/activitypub/signature.ts | |||
@@ -9,10 +9,18 @@ import { logger } from '../../../helpers/logger' | |||
9 | import { areValidationErrors } from '../utils' | 9 | import { areValidationErrors } from '../utils' |
10 | 10 | ||
11 | const signatureValidator = [ | 11 | const signatureValidator = [ |
12 | body('signature.type').custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), | 12 | body('signature.type') |
13 | body('signature.created').custom(isDateValid).withMessage('Should have a valid signature created date'), | 13 | .optional() |
14 | body('signature.creator').custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), | 14 | .custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), |
15 | body('signature.signatureValue').custom(isSignatureValueValid).withMessage('Should have a valid signature value'), | 15 | body('signature.created') |
16 | .optional() | ||
17 | .custom(isDateValid).withMessage('Should have a valid signature created date'), | ||
18 | body('signature.creator') | ||
19 | .optional() | ||
20 | .custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), | ||
21 | body('signature.signatureValue') | ||
22 | .optional() | ||
23 | .custom(isSignatureValueValid).withMessage('Should have a valid signature value'), | ||
16 | 24 | ||
17 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 25 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
18 | logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) | 26 | logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) |
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts new file mode 100644 index 000000000..109276c63 --- /dev/null +++ b/server/middlewares/validators/blocklist.ts | |||
@@ -0,0 +1,172 @@ | |||
1 | import { body, param } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | import { logger } from '../../helpers/logger' | ||
4 | import { areValidationErrors } from './utils' | ||
5 | import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' | ||
6 | import { UserModel } from '../../models/account/user' | ||
7 | import { AccountBlocklistModel } from '../../models/account/account-blocklist' | ||
8 | import { isHostValid } from '../../helpers/custom-validators/servers' | ||
9 | import { ServerBlocklistModel } from '../../models/server/server-blocklist' | ||
10 | import { ServerModel } from '../../models/server/server' | ||
11 | import { CONFIG } from '../../initializers' | ||
12 | import { getServerActor } from '../../helpers/utils' | ||
13 | |||
14 | const blockAccountValidator = [ | ||
15 | body('accountName').exists().withMessage('Should have an account name with host'), | ||
16 | |||
17 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
18 | logger.debug('Checking blockAccountByAccountValidator parameters', { parameters: req.body }) | ||
19 | |||
20 | if (areValidationErrors(req, res)) return | ||
21 | if (!await isAccountNameWithHostExist(req.body.accountName, res)) return | ||
22 | |||
23 | const user = res.locals.oauth.token.User as UserModel | ||
24 | const accountToBlock = res.locals.account | ||
25 | |||
26 | if (user.Account.id === accountToBlock.id) { | ||
27 | res.status(409) | ||
28 | .send({ error: 'You cannot block yourself.' }) | ||
29 | .end() | ||
30 | |||
31 | return | ||
32 | } | ||
33 | |||
34 | return next() | ||
35 | } | ||
36 | ] | ||
37 | |||
38 | const unblockAccountByAccountValidator = [ | ||
39 | param('accountName').exists().withMessage('Should have an account name with host'), | ||
40 | |||
41 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
42 | logger.debug('Checking unblockAccountByAccountValidator parameters', { parameters: req.params }) | ||
43 | |||
44 | if (areValidationErrors(req, res)) return | ||
45 | if (!await isAccountNameWithHostExist(req.params.accountName, res)) return | ||
46 | |||
47 | const user = res.locals.oauth.token.User as UserModel | ||
48 | const targetAccount = res.locals.account | ||
49 | if (!await isUnblockAccountExists(user.Account.id, targetAccount.id, res)) return | ||
50 | |||
51 | return next() | ||
52 | } | ||
53 | ] | ||
54 | |||
55 | const unblockAccountByServerValidator = [ | ||
56 | param('accountName').exists().withMessage('Should have an account name with host'), | ||
57 | |||
58 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
59 | logger.debug('Checking unblockAccountByServerValidator parameters', { parameters: req.params }) | ||
60 | |||
61 | if (areValidationErrors(req, res)) return | ||
62 | if (!await isAccountNameWithHostExist(req.params.accountName, res)) return | ||
63 | |||
64 | const serverActor = await getServerActor() | ||
65 | const targetAccount = res.locals.account | ||
66 | if (!await isUnblockAccountExists(serverActor.Account.id, targetAccount.id, res)) return | ||
67 | |||
68 | return next() | ||
69 | } | ||
70 | ] | ||
71 | |||
72 | const blockServerValidator = [ | ||
73 | body('host').custom(isHostValid).withMessage('Should have a valid host'), | ||
74 | |||
75 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
76 | logger.debug('Checking serverGetValidator parameters', { parameters: req.body }) | ||
77 | |||
78 | if (areValidationErrors(req, res)) return | ||
79 | |||
80 | const host: string = req.body.host | ||
81 | |||
82 | if (host === CONFIG.WEBSERVER.HOST) { | ||
83 | return res.status(409) | ||
84 | .send({ error: 'You cannot block your own server.' }) | ||
85 | .end() | ||
86 | } | ||
87 | |||
88 | const server = await ServerModel.loadByHost(host) | ||
89 | if (!server) { | ||
90 | return res.status(404) | ||
91 | .send({ error: 'Server host not found.' }) | ||
92 | .end() | ||
93 | } | ||
94 | |||
95 | res.locals.server = server | ||
96 | |||
97 | return next() | ||
98 | } | ||
99 | ] | ||
100 | |||
101 | const unblockServerByAccountValidator = [ | ||
102 | param('host').custom(isHostValid).withMessage('Should have an account name with host'), | ||
103 | |||
104 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
105 | logger.debug('Checking unblockServerByAccountValidator parameters', { parameters: req.params }) | ||
106 | |||
107 | if (areValidationErrors(req, res)) return | ||
108 | |||
109 | const user = res.locals.oauth.token.User as UserModel | ||
110 | if (!await isUnblockServerExists(user.Account.id, req.params.host, res)) return | ||
111 | |||
112 | return next() | ||
113 | } | ||
114 | ] | ||
115 | |||
116 | const unblockServerByServerValidator = [ | ||
117 | param('host').custom(isHostValid).withMessage('Should have an account name with host'), | ||
118 | |||
119 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
120 | logger.debug('Checking unblockServerByServerValidator parameters', { parameters: req.params }) | ||
121 | |||
122 | if (areValidationErrors(req, res)) return | ||
123 | |||
124 | const serverActor = await getServerActor() | ||
125 | if (!await isUnblockServerExists(serverActor.Account.id, req.params.host, res)) return | ||
126 | |||
127 | return next() | ||
128 | } | ||
129 | ] | ||
130 | |||
131 | // --------------------------------------------------------------------------- | ||
132 | |||
133 | export { | ||
134 | blockServerValidator, | ||
135 | blockAccountValidator, | ||
136 | unblockAccountByAccountValidator, | ||
137 | unblockServerByAccountValidator, | ||
138 | unblockAccountByServerValidator, | ||
139 | unblockServerByServerValidator | ||
140 | } | ||
141 | |||
142 | // --------------------------------------------------------------------------- | ||
143 | |||
144 | async function isUnblockAccountExists (accountId: number, targetAccountId: number, res: express.Response) { | ||
145 | const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) | ||
146 | if (!accountBlock) { | ||
147 | res.status(404) | ||
148 | .send({ error: 'Account block entry not found.' }) | ||
149 | .end() | ||
150 | |||
151 | return false | ||
152 | } | ||
153 | |||
154 | res.locals.accountBlock = accountBlock | ||
155 | |||
156 | return true | ||
157 | } | ||
158 | |||
159 | async function isUnblockServerExists (accountId: number, host: string, res: express.Response) { | ||
160 | const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) | ||
161 | if (!serverBlock) { | ||
162 | res.status(404) | ||
163 | .send({ error: 'Server block entry not found.' }) | ||
164 | .end() | ||
165 | |||
166 | return false | ||
167 | } | ||
168 | |||
169 | res.locals.serverBlock = serverBlock | ||
170 | |||
171 | return true | ||
172 | } | ||
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index f3f257d57..90108fa82 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -1,29 +1,44 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body } from 'express-validator/check' | 2 | import { body } from 'express-validator/check' |
3 | import { isUserNSFWPolicyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' | 3 | import { isUserNSFWPolicyValid, isUserVideoQuotaValid, isUserVideoQuotaDailyValid } from '../../helpers/custom-validators/users' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from './utils' |
6 | 6 | ||
7 | const customConfigUpdateValidator = [ | 7 | const customConfigUpdateValidator = [ |
8 | body('instance.name').exists().withMessage('Should have a valid instance name'), | 8 | body('instance.name').exists().withMessage('Should have a valid instance name'), |
9 | body('instance.shortDescription').exists().withMessage('Should have a valid instance short description'), | ||
9 | body('instance.description').exists().withMessage('Should have a valid instance description'), | 10 | body('instance.description').exists().withMessage('Should have a valid instance description'), |
10 | body('instance.terms').exists().withMessage('Should have a valid instance terms'), | 11 | body('instance.terms').exists().withMessage('Should have a valid instance terms'), |
11 | body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'), | 12 | body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'), |
12 | body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'), | 13 | body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'), |
13 | body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), | 14 | body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), |
14 | body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), | 15 | body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), |
15 | body('cache.previews.size').isInt().withMessage('Should have a valid previews size'), | 16 | |
17 | body('services.twitter.username').exists().withMessage('Should have a valid twitter username'), | ||
18 | body('services.twitter.whitelisted').isBoolean().withMessage('Should have a valid twitter whitelisted boolean'), | ||
19 | |||
20 | body('cache.previews.size').isInt().withMessage('Should have a valid previews cache size'), | ||
21 | body('cache.captions.size').isInt().withMessage('Should have a valid captions cache size'), | ||
22 | |||
16 | body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'), | 23 | body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'), |
17 | body('signup.limit').isInt().withMessage('Should have a valid signup limit'), | 24 | body('signup.limit').isInt().withMessage('Should have a valid signup limit'), |
25 | body('signup.requiresEmailVerification').isBoolean().withMessage('Should have a valid requiresEmailVerification boolean'), | ||
26 | |||
18 | body('admin.email').isEmail().withMessage('Should have a valid administrator email'), | 27 | body('admin.email').isEmail().withMessage('Should have a valid administrator email'), |
28 | body('contactForm.enabled').isBoolean().withMessage('Should have a valid contact form enabled boolean'), | ||
29 | |||
19 | body('user.videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid video quota'), | 30 | body('user.videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid video quota'), |
31 | body('user.videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily video quota'), | ||
32 | |||
20 | body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'), | 33 | body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'), |
34 | body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'), | ||
21 | body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'), | 35 | body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'), |
22 | body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'), | 36 | body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'), |
23 | body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'), | 37 | body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'), |
24 | body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'), | 38 | body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'), |
25 | body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'), | 39 | body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'), |
26 | body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'), | 40 | body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'), |
41 | |||
27 | body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), | 42 | body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), |
28 | body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), | 43 | body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), |
29 | 44 | ||
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 17226614c..65dd00335 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './account' | 1 | export * from './account' |
2 | export * from './blocklist' | ||
2 | export * from './oembed' | 3 | export * from './oembed' |
3 | export * from './activitypub' | 4 | export * from './activitypub' |
4 | export * from './pagination' | 5 | export * from './pagination' |
@@ -10,3 +11,5 @@ export * from './user-subscriptions' | |||
10 | export * from './videos' | 11 | export * from './videos' |
11 | export * from './webfinger' | 12 | export * from './webfinger' |
12 | export * from './search' | 13 | export * from './search' |
14 | export * from './server' | ||
15 | export * from './user-history' | ||
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts index c72ab78b2..329322509 100644 --- a/server/middlewares/validators/redundancy.ts +++ b/server/middlewares/validators/redundancy.ts | |||
@@ -13,7 +13,7 @@ import { ActorFollowModel } from '../../models/activitypub/actor-follow' | |||
13 | import { SERVER_ACTOR_NAME } from '../../initializers' | 13 | import { SERVER_ACTOR_NAME } from '../../initializers' |
14 | import { ServerModel } from '../../models/server/server' | 14 | import { ServerModel } from '../../models/server/server' |
15 | 15 | ||
16 | const videoRedundancyGetValidator = [ | 16 | const videoFileRedundancyGetValidator = [ |
17 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 17 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), |
18 | param('resolution') | 18 | param('resolution') |
19 | .customSanitizer(toIntOrNull) | 19 | .customSanitizer(toIntOrNull) |
@@ -24,7 +24,7 @@ const videoRedundancyGetValidator = [ | |||
24 | .custom(exists).withMessage('Should have a valid fps'), | 24 | .custom(exists).withMessage('Should have a valid fps'), |
25 | 25 | ||
26 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 26 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
27 | logger.debug('Checking videoRedundancyGetValidator parameters', { parameters: req.params }) | 27 | logger.debug('Checking videoFileRedundancyGetValidator parameters', { parameters: req.params }) |
28 | 28 | ||
29 | if (areValidationErrors(req, res)) return | 29 | if (areValidationErrors(req, res)) return |
30 | if (!await isVideoExist(req.params.videoId, res)) return | 30 | if (!await isVideoExist(req.params.videoId, res)) return |
@@ -38,7 +38,31 @@ const videoRedundancyGetValidator = [ | |||
38 | res.locals.videoFile = videoFile | 38 | res.locals.videoFile = videoFile |
39 | 39 | ||
40 | const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) | 40 | const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) |
41 | if (!videoRedundancy)return res.status(404).json({ error: 'Video redundancy not found.' }) | 41 | if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' }) |
42 | res.locals.videoRedundancy = videoRedundancy | ||
43 | |||
44 | return next() | ||
45 | } | ||
46 | ] | ||
47 | |||
48 | const videoPlaylistRedundancyGetValidator = [ | ||
49 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | ||
50 | param('streamingPlaylistType').custom(exists).withMessage('Should have a valid streaming playlist type'), | ||
51 | |||
52 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
53 | logger.debug('Checking videoPlaylistRedundancyGetValidator parameters', { parameters: req.params }) | ||
54 | |||
55 | if (areValidationErrors(req, res)) return | ||
56 | if (!await isVideoExist(req.params.videoId, res)) return | ||
57 | |||
58 | const video: VideoModel = res.locals.video | ||
59 | const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p === req.params.streamingPlaylistType) | ||
60 | |||
61 | if (!videoStreamingPlaylist) return res.status(404).json({ error: 'Video playlist not found.' }) | ||
62 | res.locals.videoStreamingPlaylist = videoStreamingPlaylist | ||
63 | |||
64 | const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id) | ||
65 | if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' }) | ||
42 | res.locals.videoRedundancy = videoRedundancy | 66 | res.locals.videoRedundancy = videoRedundancy |
43 | 67 | ||
44 | return next() | 68 | return next() |
@@ -75,6 +99,7 @@ const updateServerRedundancyValidator = [ | |||
75 | // --------------------------------------------------------------------------- | 99 | // --------------------------------------------------------------------------- |
76 | 100 | ||
77 | export { | 101 | export { |
78 | videoRedundancyGetValidator, | 102 | videoFileRedundancyGetValidator, |
103 | videoPlaylistRedundancyGetValidator, | ||
79 | updateServerRedundancyValidator | 104 | updateServerRedundancyValidator |
80 | } | 105 | } |
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts index 8baf643a5..6a95d6095 100644 --- a/server/middlewares/validators/search.ts +++ b/server/middlewares/validators/search.ts | |||
@@ -2,8 +2,7 @@ import * as express from 'express' | |||
2 | import { areValidationErrors } from './utils' | 2 | import { areValidationErrors } from './utils' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { query } from 'express-validator/check' | 4 | import { query } from 'express-validator/check' |
5 | import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search' | 5 | import { isDateValid } from '../../helpers/custom-validators/misc' |
6 | import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc' | ||
7 | 6 | ||
8 | const videosSearchValidator = [ | 7 | const videosSearchValidator = [ |
9 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | 8 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), |
@@ -35,44 +34,9 @@ const videoChannelsSearchValidator = [ | |||
35 | } | 34 | } |
36 | ] | 35 | ] |
37 | 36 | ||
38 | const commonVideosFiltersValidator = [ | ||
39 | query('categoryOneOf') | ||
40 | .optional() | ||
41 | .customSanitizer(toArray) | ||
42 | .custom(isNumberArray).withMessage('Should have a valid one of category array'), | ||
43 | query('licenceOneOf') | ||
44 | .optional() | ||
45 | .customSanitizer(toArray) | ||
46 | .custom(isNumberArray).withMessage('Should have a valid one of licence array'), | ||
47 | query('languageOneOf') | ||
48 | .optional() | ||
49 | .customSanitizer(toArray) | ||
50 | .custom(isStringArray).withMessage('Should have a valid one of language array'), | ||
51 | query('tagsOneOf') | ||
52 | .optional() | ||
53 | .customSanitizer(toArray) | ||
54 | .custom(isStringArray).withMessage('Should have a valid one of tags array'), | ||
55 | query('tagsAllOf') | ||
56 | .optional() | ||
57 | .customSanitizer(toArray) | ||
58 | .custom(isStringArray).withMessage('Should have a valid all of tags array'), | ||
59 | query('nsfw') | ||
60 | .optional() | ||
61 | .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'), | ||
62 | |||
63 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
64 | logger.debug('Checking commons video filters query', { parameters: req.query }) | ||
65 | |||
66 | if (areValidationErrors(req, res)) return | ||
67 | |||
68 | return next() | ||
69 | } | ||
70 | ] | ||
71 | |||
72 | // --------------------------------------------------------------------------- | 37 | // --------------------------------------------------------------------------- |
73 | 38 | ||
74 | export { | 39 | export { |
75 | commonVideosFiltersValidator, | ||
76 | videoChannelsSearchValidator, | 40 | videoChannelsSearchValidator, |
77 | videosSearchValidator | 41 | videosSearchValidator |
78 | } | 42 | } |
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts new file mode 100644 index 000000000..d85afc2ff --- /dev/null +++ b/server/middlewares/validators/server.ts | |||
@@ -0,0 +1,78 @@ | |||
1 | import * as express from 'express' | ||
2 | import { logger } from '../../helpers/logger' | ||
3 | import { areValidationErrors } from './utils' | ||
4 | import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers' | ||
5 | import { ServerModel } from '../../models/server/server' | ||
6 | import { body } from 'express-validator/check' | ||
7 | import { isUserDisplayNameValid } from '../../helpers/custom-validators/users' | ||
8 | import { Emailer } from '../../lib/emailer' | ||
9 | import { Redis } from '../../lib/redis' | ||
10 | import { CONFIG } from '../../initializers/constants' | ||
11 | |||
12 | const serverGetValidator = [ | ||
13 | body('host').custom(isHostValid).withMessage('Should have a valid host'), | ||
14 | |||
15 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
16 | logger.debug('Checking serverGetValidator parameters', { parameters: req.body }) | ||
17 | |||
18 | if (areValidationErrors(req, res)) return | ||
19 | |||
20 | const server = await ServerModel.loadByHost(req.body.host) | ||
21 | if (!server) { | ||
22 | return res.status(404) | ||
23 | .send({ error: 'Server host not found.' }) | ||
24 | .end() | ||
25 | } | ||
26 | |||
27 | res.locals.server = server | ||
28 | |||
29 | return next() | ||
30 | } | ||
31 | ] | ||
32 | |||
33 | const contactAdministratorValidator = [ | ||
34 | body('fromName') | ||
35 | .custom(isUserDisplayNameValid).withMessage('Should have a valid name'), | ||
36 | body('fromEmail') | ||
37 | .isEmail().withMessage('Should have a valid email'), | ||
38 | body('body') | ||
39 | .custom(isValidContactBody).withMessage('Should have a valid body'), | ||
40 | |||
41 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
42 | logger.debug('Checking contactAdministratorValidator parameters', { parameters: req.body }) | ||
43 | |||
44 | if (areValidationErrors(req, res)) return | ||
45 | |||
46 | if (CONFIG.CONTACT_FORM.ENABLED === false) { | ||
47 | return res | ||
48 | .status(409) | ||
49 | .send({ error: 'Contact form is not enabled on this instance.' }) | ||
50 | .end() | ||
51 | } | ||
52 | |||
53 | if (Emailer.isEnabled() === false) { | ||
54 | return res | ||
55 | .status(409) | ||
56 | .send({ error: 'Emailer is not enabled on this instance.' }) | ||
57 | .end() | ||
58 | } | ||
59 | |||
60 | if (await Redis.Instance.isContactFormIpExists(req.ip)) { | ||
61 | logger.info('Refusing a contact form by %s: already sent one recently.', req.ip) | ||
62 | |||
63 | return res | ||
64 | .status(403) | ||
65 | .send({ error: 'You already sent a contact form recently.' }) | ||
66 | .end() | ||
67 | } | ||
68 | |||
69 | return next() | ||
70 | } | ||
71 | ] | ||
72 | |||
73 | // --------------------------------------------------------------------------- | ||
74 | |||
75 | export { | ||
76 | serverGetValidator, | ||
77 | contactAdministratorValidator | ||
78 | } | ||
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 08dcc2680..5ceda845f 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -16,6 +16,9 @@ const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.V | |||
16 | const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) | 16 | const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) |
17 | const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) | 17 | const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) |
18 | const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) | 18 | const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) |
19 | const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) | ||
20 | const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) | ||
21 | const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS) | ||
19 | 22 | ||
20 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 23 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
21 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 24 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
@@ -31,6 +34,9 @@ const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) | |||
31 | const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) | 34 | const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) |
32 | const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) | 35 | const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) |
33 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) | 36 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) |
37 | const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS) | ||
38 | const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS) | ||
39 | const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS) | ||
34 | 40 | ||
35 | // --------------------------------------------------------------------------- | 41 | // --------------------------------------------------------------------------- |
36 | 42 | ||
@@ -48,5 +54,8 @@ export { | |||
48 | jobsSortValidator, | 54 | jobsSortValidator, |
49 | videoCommentThreadsSortValidator, | 55 | videoCommentThreadsSortValidator, |
50 | userSubscriptionsSortValidator, | 56 | userSubscriptionsSortValidator, |
51 | videoChannelsSearchSortValidator | 57 | videoChannelsSearchSortValidator, |
58 | accountsBlocklistSortValidator, | ||
59 | serversBlocklistSortValidator, | ||
60 | userNotificationsSortValidator | ||
52 | } | 61 | } |
diff --git a/server/middlewares/validators/user-history.ts b/server/middlewares/validators/user-history.ts new file mode 100644 index 000000000..418313d09 --- /dev/null +++ b/server/middlewares/validators/user-history.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'express-validator' | ||
3 | import { body } from 'express-validator/check' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { areValidationErrors } from './utils' | ||
6 | import { isDateValid } from '../../helpers/custom-validators/misc' | ||
7 | |||
8 | const userHistoryRemoveValidator = [ | ||
9 | body('beforeDate') | ||
10 | .optional() | ||
11 | .custom(isDateValid).withMessage('Should have a valid before date'), | ||
12 | |||
13 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
14 | logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body }) | ||
15 | |||
16 | if (areValidationErrors(req, res)) return | ||
17 | |||
18 | return next() | ||
19 | } | ||
20 | ] | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | export { | ||
25 | userHistoryRemoveValidator | ||
26 | } | ||
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts new file mode 100644 index 000000000..46486e081 --- /dev/null +++ b/server/middlewares/validators/user-notifications.ts | |||
@@ -0,0 +1,63 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'express-validator' | ||
3 | import { body, query } from 'express-validator/check' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { areValidationErrors } from './utils' | ||
6 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
7 | import { isNotEmptyIntArray } from '../../helpers/custom-validators/misc' | ||
8 | |||
9 | const listUserNotificationsValidator = [ | ||
10 | query('unread') | ||
11 | .optional() | ||
12 | .toBoolean() | ||
13 | .isBoolean().withMessage('Should have a valid unread boolean'), | ||
14 | |||
15 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
16 | logger.debug('Checking listUserNotificationsValidator parameters', { parameters: req.query }) | ||
17 | |||
18 | if (areValidationErrors(req, res)) return | ||
19 | |||
20 | return next() | ||
21 | } | ||
22 | ] | ||
23 | |||
24 | const updateNotificationSettingsValidator = [ | ||
25 | body('newVideoFromSubscription') | ||
26 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'), | ||
27 | body('newCommentOnMyVideo') | ||
28 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'), | ||
29 | body('videoAbuseAsModerator') | ||
30 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'), | ||
31 | body('blacklistOnMyVideo') | ||
32 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new blacklist on my video notification setting'), | ||
33 | |||
34 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
35 | logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body }) | ||
36 | |||
37 | if (areValidationErrors(req, res)) return | ||
38 | |||
39 | return next() | ||
40 | } | ||
41 | ] | ||
42 | |||
43 | const markAsReadUserNotificationsValidator = [ | ||
44 | body('ids') | ||
45 | .optional() | ||
46 | .custom(isNotEmptyIntArray).withMessage('Should have a valid notification ids to mark as read'), | ||
47 | |||
48 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
49 | logger.debug('Checking markAsReadUserNotificationsValidator parameters', { parameters: req.body }) | ||
50 | |||
51 | if (areValidationErrors(req, res)) return | ||
52 | |||
53 | return next() | ||
54 | } | ||
55 | ] | ||
56 | |||
57 | // --------------------------------------------------------------------------- | ||
58 | |||
59 | export { | ||
60 | listUserNotificationsValidator, | ||
61 | updateNotificationSettingsValidator, | ||
62 | markAsReadUserNotificationsValidator | ||
63 | } | ||
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 61297120a..a52e3060a 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -5,15 +5,16 @@ import { body, param } from 'express-validator/check' | |||
5 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
6 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | 6 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' |
7 | import { | 7 | import { |
8 | isUserAutoPlayVideoValid, isUserBlockedReasonValid, | 8 | isUserAutoPlayVideoValid, |
9 | isUserBlockedReasonValid, | ||
9 | isUserDescriptionValid, | 10 | isUserDescriptionValid, |
10 | isUserDisplayNameValid, | 11 | isUserDisplayNameValid, |
11 | isUserNSFWPolicyValid, | 12 | isUserNSFWPolicyValid, |
12 | isUserPasswordValid, | 13 | isUserPasswordValid, |
13 | isUserRoleValid, | 14 | isUserRoleValid, |
14 | isUserUsernameValid, | 15 | isUserUsernameValid, |
15 | isUserVideoQuotaValid, | 16 | isUserVideoQuotaDailyValid, |
16 | isUserVideoQuotaDailyValid | 17 | isUserVideoQuotaValid, isUserVideosHistoryEnabledValid |
17 | } from '../../helpers/custom-validators/users' | 18 | } from '../../helpers/custom-validators/users' |
18 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 19 | import { isVideoExist } from '../../helpers/custom-validators/videos' |
19 | import { logger } from '../../helpers/logger' | 20 | import { logger } from '../../helpers/logger' |
@@ -22,7 +23,6 @@ import { Redis } from '../../lib/redis' | |||
22 | import { UserModel } from '../../models/account/user' | 23 | import { UserModel } from '../../models/account/user' |
23 | import { areValidationErrors } from './utils' | 24 | import { areValidationErrors } from './utils' |
24 | import { ActorModel } from '../../models/activitypub/actor' | 25 | import { ActorModel } from '../../models/activitypub/actor' |
25 | import { comparePassword } from '../../helpers/peertube-crypto' | ||
26 | 26 | ||
27 | const usersAddValidator = [ | 27 | const usersAddValidator = [ |
28 | body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), | 28 | body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), |
@@ -113,7 +113,9 @@ const deleteMeValidator = [ | |||
113 | 113 | ||
114 | const usersUpdateValidator = [ | 114 | const usersUpdateValidator = [ |
115 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | 115 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), |
116 | body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), | ||
116 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 117 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
118 | body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'), | ||
117 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), | 119 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), |
118 | body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), | 120 | body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), |
119 | body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), | 121 | body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), |
@@ -143,6 +145,9 @@ const usersUpdateMeValidator = [ | |||
143 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 145 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
144 | body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), | 146 | body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), |
145 | body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), | 147 | body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), |
148 | body('videosHistoryEnabled') | ||
149 | .optional() | ||
150 | .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'), | ||
146 | 151 | ||
147 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 152 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
148 | logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) | 153 | logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) |
@@ -229,6 +234,7 @@ const usersAskResetPasswordValidator = [ | |||
229 | logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) | 234 | logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) |
230 | 235 | ||
231 | if (areValidationErrors(req, res)) return | 236 | if (areValidationErrors(req, res)) return |
237 | |||
232 | const exists = await checkUserEmailExist(req.body.email, res, false) | 238 | const exists = await checkUserEmailExist(req.body.email, res, false) |
233 | if (!exists) { | 239 | if (!exists) { |
234 | logger.debug('User with email %s does not exist (asking reset password).', req.body.email) | 240 | logger.debug('User with email %s does not exist (asking reset password).', req.body.email) |
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index 294783d85..a0d585b93 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts | |||
@@ -5,4 +5,6 @@ export * from './video-channels' | |||
5 | export * from './video-comments' | 5 | export * from './video-comments' |
6 | export * from './video-imports' | 6 | export * from './video-imports' |
7 | export * from './video-watch' | 7 | export * from './video-watch' |
8 | export * from './video-rates' | ||
9 | export * from './video-shares' | ||
8 | export * from './videos' | 10 | export * from './videos' |
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts index 13da7acff..2688f63ae 100644 --- a/server/middlewares/validators/videos/video-blacklist.ts +++ b/server/middlewares/validators/videos/video-blacklist.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator/check' | 2 | import { body, param } from 'express-validator/check' |
3 | import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' | 3 | import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' |
4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' | 4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
5 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
6 | import { areValidationErrors } from '../utils' | 6 | import { areValidationErrors } from '../utils' |
7 | import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' | 7 | import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' |
8 | import { VideoModel } from '../../../models/video/video' | ||
8 | 9 | ||
9 | const videosBlacklistRemoveValidator = [ | 10 | const videosBlacklistRemoveValidator = [ |
10 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 11 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), |
@@ -22,6 +23,10 @@ const videosBlacklistRemoveValidator = [ | |||
22 | 23 | ||
23 | const videosBlacklistAddValidator = [ | 24 | const videosBlacklistAddValidator = [ |
24 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 25 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), |
26 | body('unfederate') | ||
27 | .optional() | ||
28 | .toBoolean() | ||
29 | .custom(isBooleanValid).withMessage('Should have a valid unfederate boolean'), | ||
25 | body('reason') | 30 | body('reason') |
26 | .optional() | 31 | .optional() |
27 | .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), | 32 | .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), |
@@ -32,6 +37,14 @@ const videosBlacklistAddValidator = [ | |||
32 | if (areValidationErrors(req, res)) return | 37 | if (areValidationErrors(req, res)) return |
33 | if (!await isVideoExist(req.params.videoId, res)) return | 38 | if (!await isVideoExist(req.params.videoId, res)) return |
34 | 39 | ||
40 | const video: VideoModel = res.locals.video | ||
41 | if (req.body.unfederate === true && video.remote === true) { | ||
42 | return res | ||
43 | .status(409) | ||
44 | .send({ error: 'You cannot unfederate a remote video.' }) | ||
45 | .end() | ||
46 | } | ||
47 | |||
35 | return next() | 48 | return next() |
36 | } | 49 | } |
37 | ] | 50 | ] |
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts new file mode 100644 index 000000000..793354520 --- /dev/null +++ b/server/middlewares/validators/videos/video-rates.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'express-validator' | ||
3 | import { body, param } from 'express-validator/check' | ||
4 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' | ||
5 | import { isVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | import { areValidationErrors } from '../utils' | ||
8 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
9 | import { VideoRateType } from '../../../../shared/models/videos' | ||
10 | import { isAccountNameValid } from '../../../helpers/custom-validators/accounts' | ||
11 | |||
12 | const videoUpdateRateValidator = [ | ||
13 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
14 | body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), | ||
15 | |||
16 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
17 | logger.debug('Checking videoRate parameters', { parameters: req.body }) | ||
18 | |||
19 | if (areValidationErrors(req, res)) return | ||
20 | if (!await isVideoExist(req.params.id, res)) return | ||
21 | |||
22 | return next() | ||
23 | } | ||
24 | ] | ||
25 | |||
26 | const getAccountVideoRateValidator = function (rateType: VideoRateType) { | ||
27 | return [ | ||
28 | param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'), | ||
29 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | ||
30 | |||
31 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
32 | logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params }) | ||
33 | |||
34 | if (areValidationErrors(req, res)) return | ||
35 | |||
36 | const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, req.params.videoId) | ||
37 | if (!rate) { | ||
38 | return res.status(404) | ||
39 | .json({ error: 'Video rate not found' }) | ||
40 | .end() | ||
41 | } | ||
42 | |||
43 | res.locals.accountVideoRate = rate | ||
44 | |||
45 | return next() | ||
46 | } | ||
47 | ] | ||
48 | } | ||
49 | |||
50 | // --------------------------------------------------------------------------- | ||
51 | |||
52 | export { | ||
53 | videoUpdateRateValidator, | ||
54 | getAccountVideoRateValidator | ||
55 | } | ||
diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts new file mode 100644 index 000000000..646d7acb1 --- /dev/null +++ b/server/middlewares/validators/videos/video-shares.ts | |||
@@ -0,0 +1,38 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'express-validator' | ||
3 | import { param } from 'express-validator/check' | ||
4 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' | ||
5 | import { isVideoExist } from '../../../helpers/custom-validators/videos' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | import { VideoShareModel } from '../../../models/video/video-share' | ||
8 | import { areValidationErrors } from '../utils' | ||
9 | import { VideoModel } from '../../../models/video/video' | ||
10 | |||
11 | const videosShareValidator = [ | ||
12 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
13 | param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'), | ||
14 | |||
15 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
16 | logger.debug('Checking videoShare parameters', { parameters: req.params }) | ||
17 | |||
18 | if (areValidationErrors(req, res)) return | ||
19 | if (!await isVideoExist(req.params.id, res)) return | ||
20 | |||
21 | const video: VideoModel = res.locals.video | ||
22 | |||
23 | const share = await VideoShareModel.load(req.params.actorId, video.id) | ||
24 | if (!share) { | ||
25 | return res.status(404) | ||
26 | .end() | ||
27 | } | ||
28 | |||
29 | res.locals.videoShare = share | ||
30 | return next() | ||
31 | } | ||
32 | ] | ||
33 | |||
34 | // --------------------------------------------------------------------------- | ||
35 | |||
36 | export { | ||
37 | videosShareValidator | ||
38 | } | ||
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts index bca64662f..c38ad8a10 100644 --- a/server/middlewares/validators/videos/video-watch.ts +++ b/server/middlewares/validators/videos/video-watch.ts | |||
@@ -4,6 +4,7 @@ import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' | |||
4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' | 4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
5 | import { areValidationErrors } from '../utils' | 5 | import { areValidationErrors } from '../utils' |
6 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
7 | import { UserModel } from '../../../models/account/user' | ||
7 | 8 | ||
8 | const videoWatchingValidator = [ | 9 | const videoWatchingValidator = [ |
9 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 10 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
@@ -17,6 +18,12 @@ const videoWatchingValidator = [ | |||
17 | if (areValidationErrors(req, res)) return | 18 | if (areValidationErrors(req, res)) return |
18 | if (!await isVideoExist(req.params.videoId, res, 'id')) return | 19 | if (!await isVideoExist(req.params.videoId, res, 'id')) return |
19 | 20 | ||
21 | const user = res.locals.oauth.token.User as UserModel | ||
22 | if (user.videosHistoryEnabled === false) { | ||
23 | logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id) | ||
24 | return res.status(409).end() | ||
25 | } | ||
26 | |||
20 | return next() | 27 | return next() |
21 | } | 28 | } |
22 | ] | 29 | ] |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 27e8a7449..d9626929c 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import 'express-validator' | 2 | import 'express-validator' |
3 | import { body, param, ValidationChain } from 'express-validator/check' | 3 | import { body, param, query, ValidationChain } from 'express-validator/check' |
4 | import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' | 4 | import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' |
5 | import { | 5 | import { |
6 | isBooleanValid, | 6 | isBooleanValid, |
@@ -8,6 +8,7 @@ import { | |||
8 | isIdOrUUIDValid, | 8 | isIdOrUUIDValid, |
9 | isIdValid, | 9 | isIdValid, |
10 | isUUIDValid, | 10 | isUUIDValid, |
11 | toArray, | ||
11 | toIntOrNull, | 12 | toIntOrNull, |
12 | toValueOrNull | 13 | toValueOrNull |
13 | } from '../../../helpers/custom-validators/misc' | 14 | } from '../../../helpers/custom-validators/misc' |
@@ -19,20 +20,19 @@ import { | |||
19 | isVideoDescriptionValid, | 20 | isVideoDescriptionValid, |
20 | isVideoExist, | 21 | isVideoExist, |
21 | isVideoFile, | 22 | isVideoFile, |
23 | isVideoFilterValid, | ||
22 | isVideoImage, | 24 | isVideoImage, |
23 | isVideoLanguageValid, | 25 | isVideoLanguageValid, |
24 | isVideoLicenceValid, | 26 | isVideoLicenceValid, |
25 | isVideoNameValid, | 27 | isVideoNameValid, |
26 | isVideoPrivacyValid, | 28 | isVideoPrivacyValid, |
27 | isVideoRatingTypeValid, | ||
28 | isVideoSupportValid, | 29 | isVideoSupportValid, |
29 | isVideoTagsValid | 30 | isVideoTagsValid |
30 | } from '../../../helpers/custom-validators/videos' | 31 | } from '../../../helpers/custom-validators/videos' |
31 | import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' | 32 | import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' |
32 | import { logger } from '../../../helpers/logger' | 33 | import { logger } from '../../../helpers/logger' |
33 | import { CONSTRAINTS_FIELDS } from '../../../initializers' | 34 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers' |
34 | import { VideoShareModel } from '../../../models/video/video-share' | 35 | import { authenticatePromiseIfNeeded } from '../../oauth' |
35 | import { authenticate } from '../../oauth' | ||
36 | import { areValidationErrors } from '../utils' | 36 | import { areValidationErrors } from '../utils' |
37 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 37 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
38 | import { VideoModel } from '../../../models/video/video' | 38 | import { VideoModel } from '../../../models/video/video' |
@@ -42,6 +42,8 @@ import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/vid | |||
42 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' | 42 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' |
43 | import { AccountModel } from '../../../models/account/account' | 43 | import { AccountModel } from '../../../models/account/account' |
44 | import { VideoFetchType } from '../../../helpers/video' | 44 | import { VideoFetchType } from '../../../helpers/video' |
45 | import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' | ||
46 | import { getServerActor } from '../../../helpers/utils' | ||
45 | 47 | ||
46 | const videosAddValidator = getCommonVideoAttributes().concat([ | 48 | const videosAddValidator = getCommonVideoAttributes().concat([ |
47 | body('videofile') | 49 | body('videofile') |
@@ -69,7 +71,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([ | |||
69 | if (isAble === false) { | 71 | if (isAble === false) { |
70 | res.status(403) | 72 | res.status(403) |
71 | .json({ error: 'The user video quota is exceeded with this video.' }) | 73 | .json({ error: 'The user video quota is exceeded with this video.' }) |
72 | .end() | ||
73 | 74 | ||
74 | return cleanUpReqFiles(req) | 75 | return cleanUpReqFiles(req) |
75 | } | 76 | } |
@@ -82,7 +83,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([ | |||
82 | logger.error('Invalid input file in videosAddValidator.', { err }) | 83 | logger.error('Invalid input file in videosAddValidator.', { err }) |
83 | res.status(400) | 84 | res.status(400) |
84 | .json({ error: 'Invalid input file.' }) | 85 | .json({ error: 'Invalid input file.' }) |
85 | .end() | ||
86 | 86 | ||
87 | return cleanUpReqFiles(req) | 87 | return cleanUpReqFiles(req) |
88 | } | 88 | } |
@@ -120,7 +120,6 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([ | |||
120 | cleanUpReqFiles(req) | 120 | cleanUpReqFiles(req) |
121 | return res.status(409) | 121 | return res.status(409) |
122 | .json({ error: 'Cannot set "private" a video that was not private.' }) | 122 | .json({ error: 'Cannot set "private" a video that was not private.' }) |
123 | .end() | ||
124 | } | 123 | } |
125 | 124 | ||
126 | if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) | 125 | if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) |
@@ -129,6 +128,31 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([ | |||
129 | } | 128 | } |
130 | ]) | 129 | ]) |
131 | 130 | ||
131 | async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
132 | const video: VideoModel = res.locals.video | ||
133 | |||
134 | // Anybody can watch local videos | ||
135 | if (video.isOwned() === true) return next() | ||
136 | |||
137 | // Logged user | ||
138 | if (res.locals.oauth) { | ||
139 | // Users can search or watch remote videos | ||
140 | if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next() | ||
141 | } | ||
142 | |||
143 | // Anybody can search or watch remote videos | ||
144 | if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next() | ||
145 | |||
146 | // Check our instance follows an actor that shared this video | ||
147 | const serverActor = await getServerActor() | ||
148 | if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next() | ||
149 | |||
150 | return res.status(403) | ||
151 | .json({ | ||
152 | error: 'Cannot get this video regarding follow constraints.' | ||
153 | }) | ||
154 | } | ||
155 | |||
132 | const videosCustomGetValidator = (fetchType: VideoFetchType) => { | 156 | const videosCustomGetValidator = (fetchType: VideoFetchType) => { |
133 | return [ | 157 | return [ |
134 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 158 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
@@ -143,18 +167,20 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => { | |||
143 | 167 | ||
144 | // Video private or blacklisted | 168 | // Video private or blacklisted |
145 | if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { | 169 | if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { |
146 | return authenticate(req, res, () => { | 170 | await authenticatePromiseIfNeeded(req, res) |
147 | const user: UserModel = res.locals.oauth.token.User | 171 | |
148 | 172 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null | |
149 | // Only the owner or a user that have blacklist rights can see the video | 173 | |
150 | if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) { | 174 | // Only the owner or a user that have blacklist rights can see the video |
151 | return res.status(403) | 175 | if ( |
152 | .json({ error: 'Cannot get this private or blacklisted video.' }) | 176 | !user || |
153 | .end() | 177 | (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) |
154 | } | 178 | ) { |
155 | 179 | return res.status(403) | |
156 | return next() | 180 | .json({ error: 'Cannot get this private or blacklisted video.' }) |
157 | }) | 181 | } |
182 | |||
183 | return next() | ||
158 | } | 184 | } |
159 | 185 | ||
160 | // Video is public, anyone can access it | 186 | // Video is public, anyone can access it |
@@ -189,41 +215,6 @@ const videosRemoveValidator = [ | |||
189 | } | 215 | } |
190 | ] | 216 | ] |
191 | 217 | ||
192 | const videoRateValidator = [ | ||
193 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
194 | body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), | ||
195 | |||
196 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
197 | logger.debug('Checking videoRate parameters', { parameters: req.body }) | ||
198 | |||
199 | if (areValidationErrors(req, res)) return | ||
200 | if (!await isVideoExist(req.params.id, res)) return | ||
201 | |||
202 | return next() | ||
203 | } | ||
204 | ] | ||
205 | |||
206 | const videosShareValidator = [ | ||
207 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
208 | param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'), | ||
209 | |||
210 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
211 | logger.debug('Checking videoShare parameters', { parameters: req.params }) | ||
212 | |||
213 | if (areValidationErrors(req, res)) return | ||
214 | if (!await isVideoExist(req.params.id, res)) return | ||
215 | |||
216 | const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined) | ||
217 | if (!share) { | ||
218 | return res.status(404) | ||
219 | .end() | ||
220 | } | ||
221 | |||
222 | res.locals.videoShare = share | ||
223 | return next() | ||
224 | } | ||
225 | ] | ||
226 | |||
227 | const videosChangeOwnershipValidator = [ | 218 | const videosChangeOwnershipValidator = [ |
228 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 219 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
229 | 220 | ||
@@ -239,8 +230,8 @@ const videosChangeOwnershipValidator = [ | |||
239 | const nextOwner = await AccountModel.loadLocalByName(req.body.username) | 230 | const nextOwner = await AccountModel.loadLocalByName(req.body.username) |
240 | if (!nextOwner) { | 231 | if (!nextOwner) { |
241 | res.status(400) | 232 | res.status(400) |
242 | .type('json') | 233 | .json({ error: 'Changing video ownership to a remote account is not supported yet' }) |
243 | .end() | 234 | |
244 | return | 235 | return |
245 | } | 236 | } |
246 | res.locals.nextOwner = nextOwner | 237 | res.locals.nextOwner = nextOwner |
@@ -271,7 +262,7 @@ const videosTerminateChangeOwnershipValidator = [ | |||
271 | } else { | 262 | } else { |
272 | res.status(403) | 263 | res.status(403) |
273 | .json({ error: 'Ownership already accepted or refused' }) | 264 | .json({ error: 'Ownership already accepted or refused' }) |
274 | .end() | 265 | |
275 | return | 266 | return |
276 | } | 267 | } |
277 | } | 268 | } |
@@ -288,7 +279,7 @@ const videosAcceptChangeOwnershipValidator = [ | |||
288 | if (isAble === false) { | 279 | if (isAble === false) { |
289 | res.status(403) | 280 | res.status(403) |
290 | .json({ error: 'The user video quota is exceeded with this video.' }) | 281 | .json({ error: 'The user video quota is exceeded with this video.' }) |
291 | .end() | 282 | |
292 | return | 283 | return |
293 | } | 284 | } |
294 | 285 | ||
@@ -367,23 +358,68 @@ function getCommonVideoAttributes () { | |||
367 | ] as (ValidationChain | express.Handler)[] | 358 | ] as (ValidationChain | express.Handler)[] |
368 | } | 359 | } |
369 | 360 | ||
361 | const commonVideosFiltersValidator = [ | ||
362 | query('categoryOneOf') | ||
363 | .optional() | ||
364 | .customSanitizer(toArray) | ||
365 | .custom(isNumberArray).withMessage('Should have a valid one of category array'), | ||
366 | query('licenceOneOf') | ||
367 | .optional() | ||
368 | .customSanitizer(toArray) | ||
369 | .custom(isNumberArray).withMessage('Should have a valid one of licence array'), | ||
370 | query('languageOneOf') | ||
371 | .optional() | ||
372 | .customSanitizer(toArray) | ||
373 | .custom(isStringArray).withMessage('Should have a valid one of language array'), | ||
374 | query('tagsOneOf') | ||
375 | .optional() | ||
376 | .customSanitizer(toArray) | ||
377 | .custom(isStringArray).withMessage('Should have a valid one of tags array'), | ||
378 | query('tagsAllOf') | ||
379 | .optional() | ||
380 | .customSanitizer(toArray) | ||
381 | .custom(isStringArray).withMessage('Should have a valid all of tags array'), | ||
382 | query('nsfw') | ||
383 | .optional() | ||
384 | .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'), | ||
385 | query('filter') | ||
386 | .optional() | ||
387 | .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'), | ||
388 | |||
389 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
390 | logger.debug('Checking commons video filters query', { parameters: req.query }) | ||
391 | |||
392 | if (areValidationErrors(req, res)) return | ||
393 | |||
394 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
395 | if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) { | ||
396 | res.status(401) | ||
397 | .json({ error: 'You are not allowed to see all local videos.' }) | ||
398 | |||
399 | return | ||
400 | } | ||
401 | |||
402 | return next() | ||
403 | } | ||
404 | ] | ||
405 | |||
370 | // --------------------------------------------------------------------------- | 406 | // --------------------------------------------------------------------------- |
371 | 407 | ||
372 | export { | 408 | export { |
373 | videosAddValidator, | 409 | videosAddValidator, |
374 | videosUpdateValidator, | 410 | videosUpdateValidator, |
375 | videosGetValidator, | 411 | videosGetValidator, |
412 | checkVideoFollowConstraints, | ||
376 | videosCustomGetValidator, | 413 | videosCustomGetValidator, |
377 | videosRemoveValidator, | 414 | videosRemoveValidator, |
378 | videosShareValidator, | ||
379 | |||
380 | videoRateValidator, | ||
381 | 415 | ||
382 | videosChangeOwnershipValidator, | 416 | videosChangeOwnershipValidator, |
383 | videosTerminateChangeOwnershipValidator, | 417 | videosTerminateChangeOwnershipValidator, |
384 | videosAcceptChangeOwnershipValidator, | 418 | videosAcceptChangeOwnershipValidator, |
385 | 419 | ||
386 | getCommonVideoAttributes | 420 | getCommonVideoAttributes, |
421 | |||
422 | commonVideosFiltersValidator | ||
387 | } | 423 | } |
388 | 424 | ||
389 | // --------------------------------------------------------------------------- | 425 | // --------------------------------------------------------------------------- |
@@ -391,9 +427,10 @@ export { | |||
391 | function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { | 427 | function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { |
392 | if (req.body.scheduleUpdate) { | 428 | if (req.body.scheduleUpdate) { |
393 | if (!req.body.scheduleUpdate.updateAt) { | 429 | if (!req.body.scheduleUpdate.updateAt) { |
430 | logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.') | ||
431 | |||
394 | res.status(400) | 432 | res.status(400) |
395 | .json({ error: 'Schedule update at is mandatory.' }) | 433 | .json({ error: 'Schedule update at is mandatory.' }) |
396 | .end() | ||
397 | 434 | ||
398 | return true | 435 | return true |
399 | } | 436 | } |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts new file mode 100644 index 000000000..efd6ed59e --- /dev/null +++ b/server/models/account/account-blocklist.ts | |||
@@ -0,0 +1,142 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { AccountModel } from './account' | ||
3 | import { getSort } from '../utils' | ||
4 | import { AccountBlock } from '../../../shared/models/blocklist' | ||
5 | import { Op } from 'sequelize' | ||
6 | |||
7 | enum ScopeNames { | ||
8 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' | ||
9 | } | ||
10 | |||
11 | @Scopes({ | ||
12 | [ScopeNames.WITH_ACCOUNTS]: { | ||
13 | include: [ | ||
14 | { | ||
15 | model: () => AccountModel, | ||
16 | required: true, | ||
17 | as: 'ByAccount' | ||
18 | }, | ||
19 | { | ||
20 | model: () => AccountModel, | ||
21 | required: true, | ||
22 | as: 'BlockedAccount' | ||
23 | } | ||
24 | ] | ||
25 | } | ||
26 | }) | ||
27 | |||
28 | @Table({ | ||
29 | tableName: 'accountBlocklist', | ||
30 | indexes: [ | ||
31 | { | ||
32 | fields: [ 'accountId', 'targetAccountId' ], | ||
33 | unique: true | ||
34 | }, | ||
35 | { | ||
36 | fields: [ 'targetAccountId' ] | ||
37 | } | ||
38 | ] | ||
39 | }) | ||
40 | export class AccountBlocklistModel extends Model<AccountBlocklistModel> { | ||
41 | |||
42 | @CreatedAt | ||
43 | createdAt: Date | ||
44 | |||
45 | @UpdatedAt | ||
46 | updatedAt: Date | ||
47 | |||
48 | @ForeignKey(() => AccountModel) | ||
49 | @Column | ||
50 | accountId: number | ||
51 | |||
52 | @BelongsTo(() => AccountModel, { | ||
53 | foreignKey: { | ||
54 | name: 'accountId', | ||
55 | allowNull: false | ||
56 | }, | ||
57 | as: 'ByAccount', | ||
58 | onDelete: 'CASCADE' | ||
59 | }) | ||
60 | ByAccount: AccountModel | ||
61 | |||
62 | @ForeignKey(() => AccountModel) | ||
63 | @Column | ||
64 | targetAccountId: number | ||
65 | |||
66 | @BelongsTo(() => AccountModel, { | ||
67 | foreignKey: { | ||
68 | name: 'targetAccountId', | ||
69 | allowNull: false | ||
70 | }, | ||
71 | as: 'BlockedAccount', | ||
72 | onDelete: 'CASCADE' | ||
73 | }) | ||
74 | BlockedAccount: AccountModel | ||
75 | |||
76 | static isAccountMutedBy (accountId: number, targetAccountId: number) { | ||
77 | return AccountBlocklistModel.isAccountMutedByMulti([ accountId ], targetAccountId) | ||
78 | .then(result => result[accountId]) | ||
79 | } | ||
80 | |||
81 | static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) { | ||
82 | const query = { | ||
83 | attributes: [ 'accountId', 'id' ], | ||
84 | where: { | ||
85 | accountId: { | ||
86 | [Op.any]: accountIds | ||
87 | }, | ||
88 | targetAccountId | ||
89 | }, | ||
90 | raw: true | ||
91 | } | ||
92 | |||
93 | return AccountBlocklistModel.unscoped() | ||
94 | .findAll(query) | ||
95 | .then(rows => { | ||
96 | const result: { [accountId: number]: boolean } = {} | ||
97 | |||
98 | for (const accountId of accountIds) { | ||
99 | result[accountId] = !!rows.find(r => r.accountId === accountId) | ||
100 | } | ||
101 | |||
102 | return result | ||
103 | }) | ||
104 | } | ||
105 | |||
106 | static loadByAccountAndTarget (accountId: number, targetAccountId: number) { | ||
107 | const query = { | ||
108 | where: { | ||
109 | accountId, | ||
110 | targetAccountId | ||
111 | } | ||
112 | } | ||
113 | |||
114 | return AccountBlocklistModel.findOne(query) | ||
115 | } | ||
116 | |||
117 | static listForApi (accountId: number, start: number, count: number, sort: string) { | ||
118 | const query = { | ||
119 | offset: start, | ||
120 | limit: count, | ||
121 | order: getSort(sort), | ||
122 | where: { | ||
123 | accountId | ||
124 | } | ||
125 | } | ||
126 | |||
127 | return AccountBlocklistModel | ||
128 | .scope([ ScopeNames.WITH_ACCOUNTS ]) | ||
129 | .findAndCountAll(query) | ||
130 | .then(({ rows, count }) => { | ||
131 | return { total: count, data: rows } | ||
132 | }) | ||
133 | } | ||
134 | |||
135 | toFormattedJSON (): AccountBlock { | ||
136 | return { | ||
137 | byAccount: this.ByAccount.toFormattedJSON(), | ||
138 | blockedAccount: this.BlockedAccount.toFormattedJSON(), | ||
139 | createdAt: this.createdAt | ||
140 | } | ||
141 | } | ||
142 | } | ||
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index c99e32012..18762f0c5 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -1,12 +1,14 @@ | |||
1 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
2 | import { Transaction } from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
3 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 3 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
4 | import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' | 4 | import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' |
5 | import { VideoRateType } from '../../../shared/models/videos' | 5 | import { VideoRateType } from '../../../shared/models/videos' |
6 | import { VIDEO_RATE_TYPES } from '../../initializers' | 6 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers' |
7 | import { VideoModel } from '../video/video' | 7 | import { VideoModel } from '../video/video' |
8 | import { AccountModel } from './account' | 8 | import { AccountModel } from './account' |
9 | import { ActorModel } from '../activitypub/actor' | 9 | import { ActorModel } from '../activitypub/actor' |
10 | import { throwIfNotValid } from '../utils' | ||
11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
10 | 12 | ||
11 | /* | 13 | /* |
12 | Account rates per video. | 14 | Account rates per video. |
@@ -26,6 +28,10 @@ import { ActorModel } from '../activitypub/actor' | |||
26 | }, | 28 | }, |
27 | { | 29 | { |
28 | fields: [ 'videoId', 'type' ] | 30 | fields: [ 'videoId', 'type' ] |
31 | }, | ||
32 | { | ||
33 | fields: [ 'url' ], | ||
34 | unique: true | ||
29 | } | 35 | } |
30 | ] | 36 | ] |
31 | }) | 37 | }) |
@@ -35,6 +41,11 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> { | |||
35 | @Column(DataType.ENUM(values(VIDEO_RATE_TYPES))) | 41 | @Column(DataType.ENUM(values(VIDEO_RATE_TYPES))) |
36 | type: VideoRateType | 42 | type: VideoRateType |
37 | 43 | ||
44 | @AllowNull(false) | ||
45 | @Is('AccountVideoRateUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) | ||
46 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_RATES.URL.max)) | ||
47 | url: string | ||
48 | |||
38 | @CreatedAt | 49 | @CreatedAt |
39 | createdAt: Date | 50 | createdAt: Date |
40 | 51 | ||
@@ -65,7 +76,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> { | |||
65 | }) | 76 | }) |
66 | Account: AccountModel | 77 | Account: AccountModel |
67 | 78 | ||
68 | static load (accountId: number, videoId: number, transaction: Transaction) { | 79 | static load (accountId: number, videoId: number, transaction?: Transaction) { |
69 | const options: IFindOptions<AccountVideoRateModel> = { | 80 | const options: IFindOptions<AccountVideoRateModel> = { |
70 | where: { | 81 | where: { |
71 | accountId, | 82 | accountId, |
@@ -77,6 +88,49 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> { | |||
77 | return AccountVideoRateModel.findOne(options) | 88 | return AccountVideoRateModel.findOne(options) |
78 | } | 89 | } |
79 | 90 | ||
91 | static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) { | ||
92 | const options: IFindOptions<AccountVideoRateModel> = { | ||
93 | where: { | ||
94 | videoId, | ||
95 | type: rateType | ||
96 | }, | ||
97 | include: [ | ||
98 | { | ||
99 | model: AccountModel.unscoped(), | ||
100 | required: true, | ||
101 | include: [ | ||
102 | { | ||
103 | attributes: [ 'id', 'url', 'preferredUsername' ], | ||
104 | model: ActorModel.unscoped(), | ||
105 | required: true, | ||
106 | where: { | ||
107 | preferredUsername: accountName | ||
108 | } | ||
109 | } | ||
110 | ] | ||
111 | }, | ||
112 | { | ||
113 | model: VideoModel.unscoped(), | ||
114 | required: true | ||
115 | } | ||
116 | ] | ||
117 | } | ||
118 | if (transaction) options.transaction = transaction | ||
119 | |||
120 | return AccountVideoRateModel.findOne(options) | ||
121 | } | ||
122 | |||
123 | static loadByUrl (url: string, transaction: Transaction) { | ||
124 | const options: IFindOptions<AccountVideoRateModel> = { | ||
125 | where: { | ||
126 | url | ||
127 | } | ||
128 | } | ||
129 | if (transaction) options.transaction = transaction | ||
130 | |||
131 | return AccountVideoRateModel.findOne(options) | ||
132 | } | ||
133 | |||
80 | static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) { | 134 | static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) { |
81 | const query = { | 135 | const query = { |
82 | offset: start, | 136 | offset: start, |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 5a237d733..84ef0b30d 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -241,6 +241,27 @@ export class AccountModel extends Model<AccountModel> { | |||
241 | }) | 241 | }) |
242 | } | 242 | } |
243 | 243 | ||
244 | static listLocalsForSitemap (sort: string) { | ||
245 | const query = { | ||
246 | attributes: [ ], | ||
247 | offset: 0, | ||
248 | order: getSort(sort), | ||
249 | include: [ | ||
250 | { | ||
251 | attributes: [ 'preferredUsername', 'serverId' ], | ||
252 | model: ActorModel.unscoped(), | ||
253 | where: { | ||
254 | serverId: null | ||
255 | } | ||
256 | } | ||
257 | ] | ||
258 | } | ||
259 | |||
260 | return AccountModel | ||
261 | .unscoped() | ||
262 | .findAll(query) | ||
263 | } | ||
264 | |||
244 | toFormattedJSON (): Account { | 265 | toFormattedJSON (): Account { |
245 | const actor = this.Actor.toFormattedJSON() | 266 | const actor = this.Actor.toFormattedJSON() |
246 | const account = { | 267 | const account = { |
@@ -267,6 +288,10 @@ export class AccountModel extends Model<AccountModel> { | |||
267 | return this.Actor.isOwned() | 288 | return this.Actor.isOwned() |
268 | } | 289 | } |
269 | 290 | ||
291 | isOutdated () { | ||
292 | return this.Actor.isOutdated() | ||
293 | } | ||
294 | |||
270 | getDisplayName () { | 295 | getDisplayName () { |
271 | return this.name | 296 | return this.name |
272 | } | 297 | } |
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts new file mode 100644 index 000000000..f1c3ac223 --- /dev/null +++ b/server/models/account/user-notification-setting.ts | |||
@@ -0,0 +1,150 @@ | |||
1 | import { | ||
2 | AfterDestroy, | ||
3 | AfterUpdate, | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | Default, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { throwIfNotValid } from '../utils' | ||
16 | import { UserModel } from './user' | ||
17 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | ||
19 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
20 | |||
21 | @Table({ | ||
22 | tableName: 'userNotificationSetting', | ||
23 | indexes: [ | ||
24 | { | ||
25 | fields: [ 'userId' ], | ||
26 | unique: true | ||
27 | } | ||
28 | ] | ||
29 | }) | ||
30 | export class UserNotificationSettingModel extends Model<UserNotificationSettingModel> { | ||
31 | |||
32 | @AllowNull(false) | ||
33 | @Default(null) | ||
34 | @Is( | ||
35 | 'UserNotificationSettingNewVideoFromSubscription', | ||
36 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newVideoFromSubscription') | ||
37 | ) | ||
38 | @Column | ||
39 | newVideoFromSubscription: UserNotificationSettingValue | ||
40 | |||
41 | @AllowNull(false) | ||
42 | @Default(null) | ||
43 | @Is( | ||
44 | 'UserNotificationSettingNewCommentOnMyVideo', | ||
45 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newCommentOnMyVideo') | ||
46 | ) | ||
47 | @Column | ||
48 | newCommentOnMyVideo: UserNotificationSettingValue | ||
49 | |||
50 | @AllowNull(false) | ||
51 | @Default(null) | ||
52 | @Is( | ||
53 | 'UserNotificationSettingVideoAbuseAsModerator', | ||
54 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') | ||
55 | ) | ||
56 | @Column | ||
57 | videoAbuseAsModerator: UserNotificationSettingValue | ||
58 | |||
59 | @AllowNull(false) | ||
60 | @Default(null) | ||
61 | @Is( | ||
62 | 'UserNotificationSettingBlacklistOnMyVideo', | ||
63 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo') | ||
64 | ) | ||
65 | @Column | ||
66 | blacklistOnMyVideo: UserNotificationSettingValue | ||
67 | |||
68 | @AllowNull(false) | ||
69 | @Default(null) | ||
70 | @Is( | ||
71 | 'UserNotificationSettingMyVideoPublished', | ||
72 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoPublished') | ||
73 | ) | ||
74 | @Column | ||
75 | myVideoPublished: UserNotificationSettingValue | ||
76 | |||
77 | @AllowNull(false) | ||
78 | @Default(null) | ||
79 | @Is( | ||
80 | 'UserNotificationSettingMyVideoImportFinished', | ||
81 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoImportFinished') | ||
82 | ) | ||
83 | @Column | ||
84 | myVideoImportFinished: UserNotificationSettingValue | ||
85 | |||
86 | @AllowNull(false) | ||
87 | @Default(null) | ||
88 | @Is( | ||
89 | 'UserNotificationSettingNewUserRegistration', | ||
90 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration') | ||
91 | ) | ||
92 | @Column | ||
93 | newUserRegistration: UserNotificationSettingValue | ||
94 | |||
95 | @AllowNull(false) | ||
96 | @Default(null) | ||
97 | @Is( | ||
98 | 'UserNotificationSettingNewFollow', | ||
99 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') | ||
100 | ) | ||
101 | @Column | ||
102 | newFollow: UserNotificationSettingValue | ||
103 | |||
104 | @AllowNull(false) | ||
105 | @Default(null) | ||
106 | @Is( | ||
107 | 'UserNotificationSettingCommentMention', | ||
108 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention') | ||
109 | ) | ||
110 | @Column | ||
111 | commentMention: UserNotificationSettingValue | ||
112 | |||
113 | @ForeignKey(() => UserModel) | ||
114 | @Column | ||
115 | userId: number | ||
116 | |||
117 | @BelongsTo(() => UserModel, { | ||
118 | foreignKey: { | ||
119 | allowNull: false | ||
120 | }, | ||
121 | onDelete: 'cascade' | ||
122 | }) | ||
123 | User: UserModel | ||
124 | |||
125 | @CreatedAt | ||
126 | createdAt: Date | ||
127 | |||
128 | @UpdatedAt | ||
129 | updatedAt: Date | ||
130 | |||
131 | @AfterUpdate | ||
132 | @AfterDestroy | ||
133 | static removeTokenCache (instance: UserNotificationSettingModel) { | ||
134 | return clearCacheByUserId(instance.userId) | ||
135 | } | ||
136 | |||
137 | toFormattedJSON (): UserNotificationSetting { | ||
138 | return { | ||
139 | newCommentOnMyVideo: this.newCommentOnMyVideo, | ||
140 | newVideoFromSubscription: this.newVideoFromSubscription, | ||
141 | videoAbuseAsModerator: this.videoAbuseAsModerator, | ||
142 | blacklistOnMyVideo: this.blacklistOnMyVideo, | ||
143 | myVideoPublished: this.myVideoPublished, | ||
144 | myVideoImportFinished: this.myVideoImportFinished, | ||
145 | newUserRegistration: this.newUserRegistration, | ||
146 | commentMention: this.commentMention, | ||
147 | newFollow: this.newFollow | ||
148 | } | ||
149 | } | ||
150 | } | ||
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts new file mode 100644 index 000000000..6cdbb827b --- /dev/null +++ b/server/models/account/user-notification.ts | |||
@@ -0,0 +1,472 @@ | |||
1 | import { | ||
2 | AllowNull, | ||
3 | BelongsTo, | ||
4 | Column, | ||
5 | CreatedAt, | ||
6 | Default, | ||
7 | ForeignKey, | ||
8 | IFindOptions, | ||
9 | Is, | ||
10 | Model, | ||
11 | Scopes, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { UserNotification, UserNotificationType } from '../../../shared' | ||
16 | import { getSort, throwIfNotValid } from '../utils' | ||
17 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | ||
18 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' | ||
19 | import { UserModel } from './user' | ||
20 | import { VideoModel } from '../video/video' | ||
21 | import { VideoCommentModel } from '../video/video-comment' | ||
22 | import { Op } from 'sequelize' | ||
23 | import { VideoChannelModel } from '../video/video-channel' | ||
24 | import { AccountModel } from './account' | ||
25 | import { VideoAbuseModel } from '../video/video-abuse' | ||
26 | import { VideoBlacklistModel } from '../video/video-blacklist' | ||
27 | import { VideoImportModel } from '../video/video-import' | ||
28 | import { ActorModel } from '../activitypub/actor' | ||
29 | import { ActorFollowModel } from '../activitypub/actor-follow' | ||
30 | import { AvatarModel } from '../avatar/avatar' | ||
31 | import { ServerModel } from '../server/server' | ||
32 | |||
33 | enum ScopeNames { | ||
34 | WITH_ALL = 'WITH_ALL' | ||
35 | } | ||
36 | |||
37 | function buildActorWithAvatarInclude () { | ||
38 | return { | ||
39 | attributes: [ 'preferredUsername' ], | ||
40 | model: () => ActorModel.unscoped(), | ||
41 | required: true, | ||
42 | include: [ | ||
43 | { | ||
44 | attributes: [ 'filename' ], | ||
45 | model: () => AvatarModel.unscoped(), | ||
46 | required: false | ||
47 | }, | ||
48 | { | ||
49 | attributes: [ 'host' ], | ||
50 | model: () => ServerModel.unscoped(), | ||
51 | required: false | ||
52 | } | ||
53 | ] | ||
54 | } | ||
55 | } | ||
56 | |||
57 | function buildVideoInclude (required: boolean) { | ||
58 | return { | ||
59 | attributes: [ 'id', 'uuid', 'name' ], | ||
60 | model: () => VideoModel.unscoped(), | ||
61 | required | ||
62 | } | ||
63 | } | ||
64 | |||
65 | function buildChannelInclude (required: boolean, withActor = false) { | ||
66 | return { | ||
67 | required, | ||
68 | attributes: [ 'id', 'name' ], | ||
69 | model: () => VideoChannelModel.unscoped(), | ||
70 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
71 | } | ||
72 | } | ||
73 | |||
74 | function buildAccountInclude (required: boolean, withActor = false) { | ||
75 | return { | ||
76 | required, | ||
77 | attributes: [ 'id', 'name' ], | ||
78 | model: () => AccountModel.unscoped(), | ||
79 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
80 | } | ||
81 | } | ||
82 | |||
83 | @Scopes({ | ||
84 | [ScopeNames.WITH_ALL]: { | ||
85 | include: [ | ||
86 | Object.assign(buildVideoInclude(false), { | ||
87 | include: [ buildChannelInclude(true, true) ] | ||
88 | }), | ||
89 | |||
90 | { | ||
91 | attributes: [ 'id', 'originCommentId' ], | ||
92 | model: () => VideoCommentModel.unscoped(), | ||
93 | required: false, | ||
94 | include: [ | ||
95 | buildAccountInclude(true, true), | ||
96 | buildVideoInclude(true) | ||
97 | ] | ||
98 | }, | ||
99 | |||
100 | { | ||
101 | attributes: [ 'id' ], | ||
102 | model: () => VideoAbuseModel.unscoped(), | ||
103 | required: false, | ||
104 | include: [ buildVideoInclude(true) ] | ||
105 | }, | ||
106 | |||
107 | { | ||
108 | attributes: [ 'id' ], | ||
109 | model: () => VideoBlacklistModel.unscoped(), | ||
110 | required: false, | ||
111 | include: [ buildVideoInclude(true) ] | ||
112 | }, | ||
113 | |||
114 | { | ||
115 | attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], | ||
116 | model: () => VideoImportModel.unscoped(), | ||
117 | required: false, | ||
118 | include: [ buildVideoInclude(false) ] | ||
119 | }, | ||
120 | |||
121 | { | ||
122 | attributes: [ 'id' ], | ||
123 | model: () => ActorFollowModel.unscoped(), | ||
124 | required: false, | ||
125 | include: [ | ||
126 | { | ||
127 | attributes: [ 'preferredUsername' ], | ||
128 | model: () => ActorModel.unscoped(), | ||
129 | required: true, | ||
130 | as: 'ActorFollower', | ||
131 | include: [ | ||
132 | { | ||
133 | attributes: [ 'id', 'name' ], | ||
134 | model: () => AccountModel.unscoped(), | ||
135 | required: true | ||
136 | }, | ||
137 | { | ||
138 | attributes: [ 'filename' ], | ||
139 | model: () => AvatarModel.unscoped(), | ||
140 | required: false | ||
141 | }, | ||
142 | { | ||
143 | attributes: [ 'host' ], | ||
144 | model: () => ServerModel.unscoped(), | ||
145 | required: false | ||
146 | } | ||
147 | ] | ||
148 | }, | ||
149 | { | ||
150 | attributes: [ 'preferredUsername' ], | ||
151 | model: () => ActorModel.unscoped(), | ||
152 | required: true, | ||
153 | as: 'ActorFollowing', | ||
154 | include: [ | ||
155 | buildChannelInclude(false), | ||
156 | buildAccountInclude(false) | ||
157 | ] | ||
158 | } | ||
159 | ] | ||
160 | }, | ||
161 | |||
162 | buildAccountInclude(false, true) | ||
163 | ] | ||
164 | } | ||
165 | }) | ||
166 | @Table({ | ||
167 | tableName: 'userNotification', | ||
168 | indexes: [ | ||
169 | { | ||
170 | fields: [ 'userId' ] | ||
171 | }, | ||
172 | { | ||
173 | fields: [ 'videoId' ], | ||
174 | where: { | ||
175 | videoId: { | ||
176 | [Op.ne]: null | ||
177 | } | ||
178 | } | ||
179 | }, | ||
180 | { | ||
181 | fields: [ 'commentId' ], | ||
182 | where: { | ||
183 | commentId: { | ||
184 | [Op.ne]: null | ||
185 | } | ||
186 | } | ||
187 | }, | ||
188 | { | ||
189 | fields: [ 'videoAbuseId' ], | ||
190 | where: { | ||
191 | videoAbuseId: { | ||
192 | [Op.ne]: null | ||
193 | } | ||
194 | } | ||
195 | }, | ||
196 | { | ||
197 | fields: [ 'videoBlacklistId' ], | ||
198 | where: { | ||
199 | videoBlacklistId: { | ||
200 | [Op.ne]: null | ||
201 | } | ||
202 | } | ||
203 | }, | ||
204 | { | ||
205 | fields: [ 'videoImportId' ], | ||
206 | where: { | ||
207 | videoImportId: { | ||
208 | [Op.ne]: null | ||
209 | } | ||
210 | } | ||
211 | }, | ||
212 | { | ||
213 | fields: [ 'accountId' ], | ||
214 | where: { | ||
215 | accountId: { | ||
216 | [Op.ne]: null | ||
217 | } | ||
218 | } | ||
219 | }, | ||
220 | { | ||
221 | fields: [ 'actorFollowId' ], | ||
222 | where: { | ||
223 | actorFollowId: { | ||
224 | [Op.ne]: null | ||
225 | } | ||
226 | } | ||
227 | } | ||
228 | ] | ||
229 | }) | ||
230 | export class UserNotificationModel extends Model<UserNotificationModel> { | ||
231 | |||
232 | @AllowNull(false) | ||
233 | @Default(null) | ||
234 | @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type')) | ||
235 | @Column | ||
236 | type: UserNotificationType | ||
237 | |||
238 | @AllowNull(false) | ||
239 | @Default(false) | ||
240 | @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read')) | ||
241 | @Column | ||
242 | read: boolean | ||
243 | |||
244 | @CreatedAt | ||
245 | createdAt: Date | ||
246 | |||
247 | @UpdatedAt | ||
248 | updatedAt: Date | ||
249 | |||
250 | @ForeignKey(() => UserModel) | ||
251 | @Column | ||
252 | userId: number | ||
253 | |||
254 | @BelongsTo(() => UserModel, { | ||
255 | foreignKey: { | ||
256 | allowNull: false | ||
257 | }, | ||
258 | onDelete: 'cascade' | ||
259 | }) | ||
260 | User: UserModel | ||
261 | |||
262 | @ForeignKey(() => VideoModel) | ||
263 | @Column | ||
264 | videoId: number | ||
265 | |||
266 | @BelongsTo(() => VideoModel, { | ||
267 | foreignKey: { | ||
268 | allowNull: true | ||
269 | }, | ||
270 | onDelete: 'cascade' | ||
271 | }) | ||
272 | Video: VideoModel | ||
273 | |||
274 | @ForeignKey(() => VideoCommentModel) | ||
275 | @Column | ||
276 | commentId: number | ||
277 | |||
278 | @BelongsTo(() => VideoCommentModel, { | ||
279 | foreignKey: { | ||
280 | allowNull: true | ||
281 | }, | ||
282 | onDelete: 'cascade' | ||
283 | }) | ||
284 | Comment: VideoCommentModel | ||
285 | |||
286 | @ForeignKey(() => VideoAbuseModel) | ||
287 | @Column | ||
288 | videoAbuseId: number | ||
289 | |||
290 | @BelongsTo(() => VideoAbuseModel, { | ||
291 | foreignKey: { | ||
292 | allowNull: true | ||
293 | }, | ||
294 | onDelete: 'cascade' | ||
295 | }) | ||
296 | VideoAbuse: VideoAbuseModel | ||
297 | |||
298 | @ForeignKey(() => VideoBlacklistModel) | ||
299 | @Column | ||
300 | videoBlacklistId: number | ||
301 | |||
302 | @BelongsTo(() => VideoBlacklistModel, { | ||
303 | foreignKey: { | ||
304 | allowNull: true | ||
305 | }, | ||
306 | onDelete: 'cascade' | ||
307 | }) | ||
308 | VideoBlacklist: VideoBlacklistModel | ||
309 | |||
310 | @ForeignKey(() => VideoImportModel) | ||
311 | @Column | ||
312 | videoImportId: number | ||
313 | |||
314 | @BelongsTo(() => VideoImportModel, { | ||
315 | foreignKey: { | ||
316 | allowNull: true | ||
317 | }, | ||
318 | onDelete: 'cascade' | ||
319 | }) | ||
320 | VideoImport: VideoImportModel | ||
321 | |||
322 | @ForeignKey(() => AccountModel) | ||
323 | @Column | ||
324 | accountId: number | ||
325 | |||
326 | @BelongsTo(() => AccountModel, { | ||
327 | foreignKey: { | ||
328 | allowNull: true | ||
329 | }, | ||
330 | onDelete: 'cascade' | ||
331 | }) | ||
332 | Account: AccountModel | ||
333 | |||
334 | @ForeignKey(() => ActorFollowModel) | ||
335 | @Column | ||
336 | actorFollowId: number | ||
337 | |||
338 | @BelongsTo(() => ActorFollowModel, { | ||
339 | foreignKey: { | ||
340 | allowNull: true | ||
341 | }, | ||
342 | onDelete: 'cascade' | ||
343 | }) | ||
344 | ActorFollow: ActorFollowModel | ||
345 | |||
346 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { | ||
347 | const query: IFindOptions<UserNotificationModel> = { | ||
348 | offset: start, | ||
349 | limit: count, | ||
350 | order: getSort(sort), | ||
351 | where: { | ||
352 | userId | ||
353 | } | ||
354 | } | ||
355 | |||
356 | if (unread !== undefined) query.where['read'] = !unread | ||
357 | |||
358 | return UserNotificationModel.scope(ScopeNames.WITH_ALL) | ||
359 | .findAndCountAll(query) | ||
360 | .then(({ rows, count }) => { | ||
361 | return { | ||
362 | data: rows, | ||
363 | total: count | ||
364 | } | ||
365 | }) | ||
366 | } | ||
367 | |||
368 | static markAsRead (userId: number, notificationIds: number[]) { | ||
369 | const query = { | ||
370 | where: { | ||
371 | userId, | ||
372 | id: { | ||
373 | [Op.any]: notificationIds | ||
374 | } | ||
375 | } | ||
376 | } | ||
377 | |||
378 | return UserNotificationModel.update({ read: true }, query) | ||
379 | } | ||
380 | |||
381 | static markAllAsRead (userId: number) { | ||
382 | const query = { where: { userId } } | ||
383 | |||
384 | return UserNotificationModel.update({ read: true }, query) | ||
385 | } | ||
386 | |||
387 | toFormattedJSON (): UserNotification { | ||
388 | const video = this.Video | ||
389 | ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) }) | ||
390 | : undefined | ||
391 | |||
392 | const videoImport = this.VideoImport ? { | ||
393 | id: this.VideoImport.id, | ||
394 | video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined, | ||
395 | torrentName: this.VideoImport.torrentName, | ||
396 | magnetUri: this.VideoImport.magnetUri, | ||
397 | targetUrl: this.VideoImport.targetUrl | ||
398 | } : undefined | ||
399 | |||
400 | const comment = this.Comment ? { | ||
401 | id: this.Comment.id, | ||
402 | threadId: this.Comment.getThreadId(), | ||
403 | account: this.formatActor(this.Comment.Account), | ||
404 | video: this.formatVideo(this.Comment.Video) | ||
405 | } : undefined | ||
406 | |||
407 | const videoAbuse = this.VideoAbuse ? { | ||
408 | id: this.VideoAbuse.id, | ||
409 | video: this.formatVideo(this.VideoAbuse.Video) | ||
410 | } : undefined | ||
411 | |||
412 | const videoBlacklist = this.VideoBlacklist ? { | ||
413 | id: this.VideoBlacklist.id, | ||
414 | video: this.formatVideo(this.VideoBlacklist.Video) | ||
415 | } : undefined | ||
416 | |||
417 | const account = this.Account ? this.formatActor(this.Account) : undefined | ||
418 | |||
419 | const actorFollow = this.ActorFollow ? { | ||
420 | id: this.ActorFollow.id, | ||
421 | follower: { | ||
422 | id: this.ActorFollow.ActorFollower.Account.id, | ||
423 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), | ||
424 | name: this.ActorFollow.ActorFollower.preferredUsername, | ||
425 | avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined, | ||
426 | host: this.ActorFollow.ActorFollower.getHost() | ||
427 | }, | ||
428 | following: { | ||
429 | type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', | ||
430 | displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(), | ||
431 | name: this.ActorFollow.ActorFollowing.preferredUsername | ||
432 | } | ||
433 | } : undefined | ||
434 | |||
435 | return { | ||
436 | id: this.id, | ||
437 | type: this.type, | ||
438 | read: this.read, | ||
439 | video, | ||
440 | videoImport, | ||
441 | comment, | ||
442 | videoAbuse, | ||
443 | videoBlacklist, | ||
444 | account, | ||
445 | actorFollow, | ||
446 | createdAt: this.createdAt.toISOString(), | ||
447 | updatedAt: this.updatedAt.toISOString() | ||
448 | } | ||
449 | } | ||
450 | |||
451 | private formatVideo (video: VideoModel) { | ||
452 | return { | ||
453 | id: video.id, | ||
454 | uuid: video.uuid, | ||
455 | name: video.name | ||
456 | } | ||
457 | } | ||
458 | |||
459 | private formatActor (accountOrChannel: AccountModel | VideoChannelModel) { | ||
460 | const avatar = accountOrChannel.Actor.Avatar | ||
461 | ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() } | ||
462 | : undefined | ||
463 | |||
464 | return { | ||
465 | id: accountOrChannel.id, | ||
466 | displayName: accountOrChannel.getDisplayName(), | ||
467 | name: accountOrChannel.Actor.preferredUsername, | ||
468 | host: accountOrChannel.Actor.getHost(), | ||
469 | avatar | ||
470 | } | ||
471 | } | ||
472 | } | ||
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts index 0476cad9d..15cb399c9 100644 --- a/server/models/account/user-video-history.ts +++ b/server/models/account/user-video-history.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { VideoModel } from '../video/video' | 2 | import { VideoModel } from '../video/video' |
3 | import { UserModel } from './user' | 3 | import { UserModel } from './user' |
4 | import { Transaction, Op, DestroyOptions } from 'sequelize' | ||
4 | 5 | ||
5 | @Table({ | 6 | @Table({ |
6 | tableName: 'userVideoHistory', | 7 | tableName: 'userVideoHistory', |
@@ -52,4 +53,34 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> { | |||
52 | onDelete: 'CASCADE' | 53 | onDelete: 'CASCADE' |
53 | }) | 54 | }) |
54 | User: UserModel | 55 | User: UserModel |
56 | |||
57 | static listForApi (user: UserModel, start: number, count: number) { | ||
58 | return VideoModel.listForApi({ | ||
59 | start, | ||
60 | count, | ||
61 | sort: '-UserVideoHistories.updatedAt', | ||
62 | nsfw: null, // All | ||
63 | includeLocalVideos: true, | ||
64 | withFiles: false, | ||
65 | user, | ||
66 | historyOfUser: user | ||
67 | }) | ||
68 | } | ||
69 | |||
70 | static removeHistoryBefore (user: UserModel, beforeDate: string, t: Transaction) { | ||
71 | const query: DestroyOptions = { | ||
72 | where: { | ||
73 | userId: user.id | ||
74 | }, | ||
75 | transaction: t | ||
76 | } | ||
77 | |||
78 | if (beforeDate) { | ||
79 | query.where.updatedAt = { | ||
80 | [Op.lt]: beforeDate | ||
81 | } | ||
82 | } | ||
83 | |||
84 | return UserVideoHistoryModel.destroy(query) | ||
85 | } | ||
55 | } | 86 | } |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index e56b0bf40..017a96657 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { | 2 | import { |
3 | AfterDelete, | 3 | AfterDestroy, |
4 | AfterUpdate, | 4 | AfterUpdate, |
5 | AllowNull, | 5 | AllowNull, |
6 | BeforeCreate, | 6 | BeforeCreate, |
@@ -31,7 +31,9 @@ import { | |||
31 | isUserRoleValid, | 31 | isUserRoleValid, |
32 | isUserUsernameValid, | 32 | isUserUsernameValid, |
33 | isUserVideoQuotaDailyValid, | 33 | isUserVideoQuotaDailyValid, |
34 | isUserVideoQuotaValid | 34 | isUserVideoQuotaValid, |
35 | isUserVideosHistoryEnabledValid, | ||
36 | isUserWebTorrentEnabledValid | ||
35 | } from '../../helpers/custom-validators/users' | 37 | } from '../../helpers/custom-validators/users' |
36 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 38 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
37 | import { OAuthTokenModel } from '../oauth/oauth-token' | 39 | import { OAuthTokenModel } from '../oauth/oauth-token' |
@@ -42,6 +44,11 @@ import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' | |||
42 | import { values } from 'lodash' | 44 | import { values } from 'lodash' |
43 | import { NSFW_POLICY_TYPES } from '../../initializers' | 45 | import { NSFW_POLICY_TYPES } from '../../initializers' |
44 | import { clearCacheByUserId } from '../../lib/oauth-model' | 46 | import { clearCacheByUserId } from '../../lib/oauth-model' |
47 | import { UserNotificationSettingModel } from './user-notification-setting' | ||
48 | import { VideoModel } from '../video/video' | ||
49 | import { ActorModel } from '../activitypub/actor' | ||
50 | import { ActorFollowModel } from '../activitypub/actor-follow' | ||
51 | import { VideoImportModel } from '../video/video-import' | ||
45 | 52 | ||
46 | enum ScopeNames { | 53 | enum ScopeNames { |
47 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' | 54 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' |
@@ -52,6 +59,10 @@ enum ScopeNames { | |||
52 | { | 59 | { |
53 | model: () => AccountModel, | 60 | model: () => AccountModel, |
54 | required: true | 61 | required: true |
62 | }, | ||
63 | { | ||
64 | model: () => UserNotificationSettingModel, | ||
65 | required: true | ||
55 | } | 66 | } |
56 | ] | 67 | ] |
57 | }) | 68 | }) |
@@ -62,6 +73,10 @@ enum ScopeNames { | |||
62 | model: () => AccountModel, | 73 | model: () => AccountModel, |
63 | required: true, | 74 | required: true, |
64 | include: [ () => VideoChannelModel ] | 75 | include: [ () => VideoChannelModel ] |
76 | }, | ||
77 | { | ||
78 | model: () => UserNotificationSettingModel, | ||
79 | required: true | ||
65 | } | 80 | } |
66 | ] | 81 | ] |
67 | } | 82 | } |
@@ -109,6 +124,18 @@ export class UserModel extends Model<UserModel> { | |||
109 | 124 | ||
110 | @AllowNull(false) | 125 | @AllowNull(false) |
111 | @Default(true) | 126 | @Default(true) |
127 | @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled')) | ||
128 | @Column | ||
129 | webTorrentEnabled: boolean | ||
130 | |||
131 | @AllowNull(false) | ||
132 | @Default(true) | ||
133 | @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled')) | ||
134 | @Column | ||
135 | videosHistoryEnabled: boolean | ||
136 | |||
137 | @AllowNull(false) | ||
138 | @Default(true) | ||
112 | @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) | 139 | @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) |
113 | @Column | 140 | @Column |
114 | autoPlayVideo: boolean | 141 | autoPlayVideo: boolean |
@@ -153,6 +180,19 @@ export class UserModel extends Model<UserModel> { | |||
153 | }) | 180 | }) |
154 | Account: AccountModel | 181 | Account: AccountModel |
155 | 182 | ||
183 | @HasOne(() => UserNotificationSettingModel, { | ||
184 | foreignKey: 'userId', | ||
185 | onDelete: 'cascade', | ||
186 | hooks: true | ||
187 | }) | ||
188 | NotificationSetting: UserNotificationSettingModel | ||
189 | |||
190 | @HasMany(() => VideoImportModel, { | ||
191 | foreignKey: 'userId', | ||
192 | onDelete: 'cascade' | ||
193 | }) | ||
194 | VideoImports: VideoImportModel[] | ||
195 | |||
156 | @HasMany(() => OAuthTokenModel, { | 196 | @HasMany(() => OAuthTokenModel, { |
157 | foreignKey: 'userId', | 197 | foreignKey: 'userId', |
158 | onDelete: 'cascade' | 198 | onDelete: 'cascade' |
@@ -172,7 +212,7 @@ export class UserModel extends Model<UserModel> { | |||
172 | } | 212 | } |
173 | 213 | ||
174 | @AfterUpdate | 214 | @AfterUpdate |
175 | @AfterDelete | 215 | @AfterDestroy |
176 | static removeTokenCache (instance: UserModel) { | 216 | static removeTokenCache (instance: UserModel) { |
177 | return clearCacheByUserId(instance.id) | 217 | return clearCacheByUserId(instance.id) |
178 | } | 218 | } |
@@ -181,7 +221,25 @@ export class UserModel extends Model<UserModel> { | |||
181 | return this.count() | 221 | return this.count() |
182 | } | 222 | } |
183 | 223 | ||
184 | static listForApi (start: number, count: number, sort: string) { | 224 | static listForApi (start: number, count: number, sort: string, search?: string) { |
225 | let where = undefined | ||
226 | if (search) { | ||
227 | where = { | ||
228 | [Sequelize.Op.or]: [ | ||
229 | { | ||
230 | email: { | ||
231 | [Sequelize.Op.iLike]: '%' + search + '%' | ||
232 | } | ||
233 | }, | ||
234 | { | ||
235 | username: { | ||
236 | [ Sequelize.Op.iLike ]: '%' + search + '%' | ||
237 | } | ||
238 | } | ||
239 | ] | ||
240 | } | ||
241 | } | ||
242 | |||
185 | const query = { | 243 | const query = { |
186 | attributes: { | 244 | attributes: { |
187 | include: [ | 245 | include: [ |
@@ -204,7 +262,8 @@ export class UserModel extends Model<UserModel> { | |||
204 | }, | 262 | }, |
205 | offset: start, | 263 | offset: start, |
206 | limit: count, | 264 | limit: count, |
207 | order: getSort(sort) | 265 | order: getSort(sort), |
266 | where | ||
208 | } | 267 | } |
209 | 268 | ||
210 | return UserModel.findAndCountAll(query) | 269 | return UserModel.findAndCountAll(query) |
@@ -216,13 +275,12 @@ export class UserModel extends Model<UserModel> { | |||
216 | }) | 275 | }) |
217 | } | 276 | } |
218 | 277 | ||
219 | static listEmailsWithRight (right: UserRight) { | 278 | static listWithRight (right: UserRight) { |
220 | const roles = Object.keys(USER_ROLE_LABELS) | 279 | const roles = Object.keys(USER_ROLE_LABELS) |
221 | .map(k => parseInt(k, 10) as UserRole) | 280 | .map(k => parseInt(k, 10) as UserRole) |
222 | .filter(role => hasUserRight(role, right)) | 281 | .filter(role => hasUserRight(role, right)) |
223 | 282 | ||
224 | const query = { | 283 | const query = { |
225 | attribute: [ 'email' ], | ||
226 | where: { | 284 | where: { |
227 | role: { | 285 | role: { |
228 | [Sequelize.Op.in]: roles | 286 | [Sequelize.Op.in]: roles |
@@ -230,9 +288,56 @@ export class UserModel extends Model<UserModel> { | |||
230 | } | 288 | } |
231 | } | 289 | } |
232 | 290 | ||
233 | return UserModel.unscoped() | 291 | return UserModel.findAll(query) |
234 | .findAll(query) | 292 | } |
235 | .then(u => u.map(u => u.email)) | 293 | |
294 | static listUserSubscribersOf (actorId: number) { | ||
295 | const query = { | ||
296 | include: [ | ||
297 | { | ||
298 | model: UserNotificationSettingModel.unscoped(), | ||
299 | required: true | ||
300 | }, | ||
301 | { | ||
302 | attributes: [ 'userId' ], | ||
303 | model: AccountModel.unscoped(), | ||
304 | required: true, | ||
305 | include: [ | ||
306 | { | ||
307 | attributes: [ ], | ||
308 | model: ActorModel.unscoped(), | ||
309 | required: true, | ||
310 | where: { | ||
311 | serverId: null | ||
312 | }, | ||
313 | include: [ | ||
314 | { | ||
315 | attributes: [ ], | ||
316 | as: 'ActorFollowings', | ||
317 | model: ActorFollowModel.unscoped(), | ||
318 | required: true, | ||
319 | where: { | ||
320 | targetActorId: actorId | ||
321 | } | ||
322 | } | ||
323 | ] | ||
324 | } | ||
325 | ] | ||
326 | } | ||
327 | ] | ||
328 | } | ||
329 | |||
330 | return UserModel.unscoped().findAll(query) | ||
331 | } | ||
332 | |||
333 | static listByUsernames (usernames: string[]) { | ||
334 | const query = { | ||
335 | where: { | ||
336 | username: usernames | ||
337 | } | ||
338 | } | ||
339 | |||
340 | return UserModel.findAll(query) | ||
236 | } | 341 | } |
237 | 342 | ||
238 | static loadById (id: number) { | 343 | static loadById (id: number) { |
@@ -281,6 +386,95 @@ export class UserModel extends Model<UserModel> { | |||
281 | return UserModel.findOne(query) | 386 | return UserModel.findOne(query) |
282 | } | 387 | } |
283 | 388 | ||
389 | static loadByVideoId (videoId: number) { | ||
390 | const query = { | ||
391 | include: [ | ||
392 | { | ||
393 | required: true, | ||
394 | attributes: [ 'id' ], | ||
395 | model: AccountModel.unscoped(), | ||
396 | include: [ | ||
397 | { | ||
398 | required: true, | ||
399 | attributes: [ 'id' ], | ||
400 | model: VideoChannelModel.unscoped(), | ||
401 | include: [ | ||
402 | { | ||
403 | required: true, | ||
404 | attributes: [ 'id' ], | ||
405 | model: VideoModel.unscoped(), | ||
406 | where: { | ||
407 | id: videoId | ||
408 | } | ||
409 | } | ||
410 | ] | ||
411 | } | ||
412 | ] | ||
413 | } | ||
414 | ] | ||
415 | } | ||
416 | |||
417 | return UserModel.findOne(query) | ||
418 | } | ||
419 | |||
420 | static loadByVideoImportId (videoImportId: number) { | ||
421 | const query = { | ||
422 | include: [ | ||
423 | { | ||
424 | required: true, | ||
425 | attributes: [ 'id' ], | ||
426 | model: VideoImportModel.unscoped(), | ||
427 | where: { | ||
428 | id: videoImportId | ||
429 | } | ||
430 | } | ||
431 | ] | ||
432 | } | ||
433 | |||
434 | return UserModel.findOne(query) | ||
435 | } | ||
436 | |||
437 | static loadByChannelActorId (videoChannelActorId: number) { | ||
438 | const query = { | ||
439 | include: [ | ||
440 | { | ||
441 | required: true, | ||
442 | attributes: [ 'id' ], | ||
443 | model: AccountModel.unscoped(), | ||
444 | include: [ | ||
445 | { | ||
446 | required: true, | ||
447 | attributes: [ 'id' ], | ||
448 | model: VideoChannelModel.unscoped(), | ||
449 | where: { | ||
450 | actorId: videoChannelActorId | ||
451 | } | ||
452 | } | ||
453 | ] | ||
454 | } | ||
455 | ] | ||
456 | } | ||
457 | |||
458 | return UserModel.findOne(query) | ||
459 | } | ||
460 | |||
461 | static loadByAccountActorId (accountActorId: number) { | ||
462 | const query = { | ||
463 | include: [ | ||
464 | { | ||
465 | required: true, | ||
466 | attributes: [ 'id' ], | ||
467 | model: AccountModel.unscoped(), | ||
468 | where: { | ||
469 | actorId: accountActorId | ||
470 | } | ||
471 | } | ||
472 | ] | ||
473 | } | ||
474 | |||
475 | return UserModel.findOne(query) | ||
476 | } | ||
477 | |||
284 | static getOriginalVideoFileTotalFromUser (user: UserModel) { | 478 | static getOriginalVideoFileTotalFromUser (user: UserModel) { |
285 | // Don't use sequelize because we need to use a sub query | 479 | // Don't use sequelize because we need to use a sub query |
286 | const query = UserModel.generateUserQuotaBaseSQL() | 480 | const query = UserModel.generateUserQuotaBaseSQL() |
@@ -336,6 +530,8 @@ export class UserModel extends Model<UserModel> { | |||
336 | email: this.email, | 530 | email: this.email, |
337 | emailVerified: this.emailVerified, | 531 | emailVerified: this.emailVerified, |
338 | nsfwPolicy: this.nsfwPolicy, | 532 | nsfwPolicy: this.nsfwPolicy, |
533 | webTorrentEnabled: this.webTorrentEnabled, | ||
534 | videosHistoryEnabled: this.videosHistoryEnabled, | ||
339 | autoPlayVideo: this.autoPlayVideo, | 535 | autoPlayVideo: this.autoPlayVideo, |
340 | role: this.role, | 536 | role: this.role, |
341 | roleLabel: USER_ROLE_LABELS[ this.role ], | 537 | roleLabel: USER_ROLE_LABELS[ this.role ], |
@@ -345,6 +541,7 @@ export class UserModel extends Model<UserModel> { | |||
345 | blocked: this.blocked, | 541 | blocked: this.blocked, |
346 | blockedReason: this.blockedReason, | 542 | blockedReason: this.blockedReason, |
347 | account: this.Account.toFormattedJSON(), | 543 | account: this.Account.toFormattedJSON(), |
544 | notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined, | ||
348 | videoChannels: [], | 545 | videoChannels: [], |
349 | videoQuotaUsed: videoQuotaUsed !== undefined | 546 | videoQuotaUsed: videoQuotaUsed !== undefined |
350 | ? parseInt(videoQuotaUsed, 10) | 547 | ? parseInt(videoQuotaUsed, 10) |
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 27bb43dae..796e07a42 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -127,22 +127,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
127 | if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) | 127 | if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) |
128 | } | 128 | } |
129 | 129 | ||
130 | static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction | undefined) { | ||
131 | if (goodInboxes.length === 0 && badInboxes.length === 0) return | ||
132 | |||
133 | logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length) | ||
134 | |||
135 | if (goodInboxes.length !== 0) { | ||
136 | ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t) | ||
137 | .catch(err => logger.error('Cannot increment scores of good actor follows.', { err })) | ||
138 | } | ||
139 | |||
140 | if (badInboxes.length !== 0) { | ||
141 | ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t) | ||
142 | .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err })) | ||
143 | } | ||
144 | } | ||
145 | |||
146 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { | 130 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { |
147 | const query = { | 131 | const query = { |
148 | where: { | 132 | where: { |
@@ -280,7 +264,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
280 | return ActorFollowModel.findAll(query) | 264 | return ActorFollowModel.findAll(query) |
281 | } | 265 | } |
282 | 266 | ||
283 | static listFollowingForApi (id: number, start: number, count: number, sort: string) { | 267 | static listFollowingForApi (id: number, start: number, count: number, sort: string, search?: string) { |
284 | const query = { | 268 | const query = { |
285 | distinct: true, | 269 | distinct: true, |
286 | offset: start, | 270 | offset: start, |
@@ -299,7 +283,17 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
299 | model: ActorModel, | 283 | model: ActorModel, |
300 | as: 'ActorFollowing', | 284 | as: 'ActorFollowing', |
301 | required: true, | 285 | required: true, |
302 | include: [ ServerModel ] | 286 | include: [ |
287 | { | ||
288 | model: ServerModel, | ||
289 | required: true, | ||
290 | where: search ? { | ||
291 | host: { | ||
292 | [Sequelize.Op.iLike]: '%' + search + '%' | ||
293 | } | ||
294 | } : undefined | ||
295 | } | ||
296 | ] | ||
303 | } | 297 | } |
304 | ] | 298 | ] |
305 | } | 299 | } |
@@ -313,7 +307,50 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
313 | }) | 307 | }) |
314 | } | 308 | } |
315 | 309 | ||
316 | static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { | 310 | static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) { |
311 | const query = { | ||
312 | distinct: true, | ||
313 | offset: start, | ||
314 | limit: count, | ||
315 | order: getSort(sort), | ||
316 | include: [ | ||
317 | { | ||
318 | model: ActorModel, | ||
319 | required: true, | ||
320 | as: 'ActorFollower', | ||
321 | include: [ | ||
322 | { | ||
323 | model: ServerModel, | ||
324 | required: true, | ||
325 | where: search ? { | ||
326 | host: { | ||
327 | [ Sequelize.Op.iLike ]: '%' + search + '%' | ||
328 | } | ||
329 | } : undefined | ||
330 | } | ||
331 | ] | ||
332 | }, | ||
333 | { | ||
334 | model: ActorModel, | ||
335 | as: 'ActorFollowing', | ||
336 | required: true, | ||
337 | where: { | ||
338 | id: actorId | ||
339 | } | ||
340 | } | ||
341 | ] | ||
342 | } | ||
343 | |||
344 | return ActorFollowModel.findAndCountAll(query) | ||
345 | .then(({ rows, count }) => { | ||
346 | return { | ||
347 | data: rows, | ||
348 | total: count | ||
349 | } | ||
350 | }) | ||
351 | } | ||
352 | |||
353 | static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) { | ||
317 | const query = { | 354 | const query = { |
318 | attributes: [], | 355 | attributes: [], |
319 | distinct: true, | 356 | distinct: true, |
@@ -321,7 +358,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
321 | limit: count, | 358 | limit: count, |
322 | order: getSort(sort), | 359 | order: getSort(sort), |
323 | where: { | 360 | where: { |
324 | actorId: id | 361 | actorId: actorId |
325 | }, | 362 | }, |
326 | include: [ | 363 | include: [ |
327 | { | 364 | { |
@@ -370,39 +407,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
370 | }) | 407 | }) |
371 | } | 408 | } |
372 | 409 | ||
373 | static listFollowersForApi (id: number, start: number, count: number, sort: string) { | ||
374 | const query = { | ||
375 | distinct: true, | ||
376 | offset: start, | ||
377 | limit: count, | ||
378 | order: getSort(sort), | ||
379 | include: [ | ||
380 | { | ||
381 | model: ActorModel, | ||
382 | required: true, | ||
383 | as: 'ActorFollower', | ||
384 | include: [ ServerModel ] | ||
385 | }, | ||
386 | { | ||
387 | model: ActorModel, | ||
388 | as: 'ActorFollowing', | ||
389 | required: true, | ||
390 | where: { | ||
391 | id | ||
392 | } | ||
393 | } | ||
394 | ] | ||
395 | } | ||
396 | |||
397 | return ActorFollowModel.findAndCountAll(query) | ||
398 | .then(({ rows, count }) => { | ||
399 | return { | ||
400 | data: rows, | ||
401 | total: count | ||
402 | } | ||
403 | }) | ||
404 | } | ||
405 | |||
406 | static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { | 410 | static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { |
407 | return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) | 411 | return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) |
408 | } | 412 | } |
@@ -444,6 +448,22 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
444 | } | 448 | } |
445 | } | 449 | } |
446 | 450 | ||
451 | static updateFollowScore (inboxUrl: string, value: number, t?: Sequelize.Transaction) { | ||
452 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + | ||
453 | 'WHERE id IN (' + | ||
454 | 'SELECT "actorFollow"."id" FROM "actorFollow" ' + | ||
455 | 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + | ||
456 | `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + | ||
457 | ')' | ||
458 | |||
459 | const options = { | ||
460 | type: Sequelize.QueryTypes.BULKUPDATE, | ||
461 | transaction: t | ||
462 | } | ||
463 | |||
464 | return ActorFollowModel.sequelize.query(query, options) | ||
465 | } | ||
466 | |||
447 | private static async createListAcceptedFollowForApiQuery ( | 467 | private static async createListAcceptedFollowForApiQuery ( |
448 | type: 'followers' | 'following', | 468 | type: 'followers' | 'following', |
449 | actorIds: number[], | 469 | actorIds: number[], |
@@ -489,33 +509,15 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
489 | tasks.push(ActorFollowModel.sequelize.query(query, options)) | 509 | tasks.push(ActorFollowModel.sequelize.query(query, options)) |
490 | } | 510 | } |
491 | 511 | ||
492 | const [ followers, [ { total } ] ] = await Promise.all(tasks) | 512 | const [ followers, [ dataTotal ] ] = await Promise.all(tasks) |
493 | const urls: string[] = followers.map(f => f.url) | 513 | const urls: string[] = followers.map(f => f.url) |
494 | 514 | ||
495 | return { | 515 | return { |
496 | data: urls, | 516 | data: urls, |
497 | total: parseInt(total, 10) | 517 | total: dataTotal ? parseInt(dataTotal.total, 10) : 0 |
498 | } | 518 | } |
499 | } | 519 | } |
500 | 520 | ||
501 | private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) { | ||
502 | const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',') | ||
503 | |||
504 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + | ||
505 | 'WHERE id IN (' + | ||
506 | 'SELECT "actorFollow"."id" FROM "actorFollow" ' + | ||
507 | 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + | ||
508 | 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' + | ||
509 | ')' | ||
510 | |||
511 | const options = t ? { | ||
512 | type: Sequelize.QueryTypes.BULKUPDATE, | ||
513 | transaction: t | ||
514 | } : undefined | ||
515 | |||
516 | return ActorFollowModel.sequelize.query(query, options) | ||
517 | } | ||
518 | |||
519 | private static listBadActorFollows () { | 521 | private static listBadActorFollows () { |
520 | const query = { | 522 | const query = { |
521 | where: { | 523 | where: { |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 12b83916e..dda57a8ba 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -219,6 +219,7 @@ export class ActorModel extends Model<ActorModel> { | |||
219 | name: 'actorId', | 219 | name: 'actorId', |
220 | allowNull: false | 220 | allowNull: false |
221 | }, | 221 | }, |
222 | as: 'ActorFollowings', | ||
222 | onDelete: 'cascade' | 223 | onDelete: 'cascade' |
223 | }) | 224 | }) |
224 | ActorFollowing: ActorFollowModel[] | 225 | ActorFollowing: ActorFollowModel[] |
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts index 5d73e24fa..303aebcc2 100644 --- a/server/models/avatar/avatar.ts +++ b/server/models/avatar/avatar.ts | |||
@@ -23,7 +23,10 @@ export class AvatarModel extends Model<AvatarModel> { | |||
23 | @AfterDestroy | 23 | @AfterDestroy |
24 | static removeFilesAndSendDelete (instance: AvatarModel) { | 24 | static removeFilesAndSendDelete (instance: AvatarModel) { |
25 | logger.info('Removing avatar file %s.', instance.filename) | 25 | logger.info('Removing avatar file %s.', instance.filename) |
26 | return instance.removeAvatar() | 26 | |
27 | // Don't block the transaction | ||
28 | instance.removeAvatar() | ||
29 | .catch(err => logger.error('Cannot remove avatar file %s.', instance.filename, err)) | ||
27 | } | 30 | } |
28 | 31 | ||
29 | toFormattedJSON (): Avatar { | 32 | toFormattedJSON (): Avatar { |
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index ef9592c04..08d892da4 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { | 1 | import { |
2 | AfterDelete, | 2 | AfterDestroy, |
3 | AfterUpdate, | 3 | AfterUpdate, |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -47,7 +47,7 @@ enum ScopeNames { | |||
47 | required: true, | 47 | required: true, |
48 | include: [ | 48 | include: [ |
49 | { | 49 | { |
50 | attributes: [ 'id' ], | 50 | attributes: [ 'id', 'url' ], |
51 | model: () => ActorModel.unscoped(), | 51 | model: () => ActorModel.unscoped(), |
52 | required: true | 52 | required: true |
53 | } | 53 | } |
@@ -126,7 +126,7 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> { | |||
126 | OAuthClients: OAuthClientModel[] | 126 | OAuthClients: OAuthClientModel[] |
127 | 127 | ||
128 | @AfterUpdate | 128 | @AfterUpdate |
129 | @AfterDelete | 129 | @AfterDestroy |
130 | static removeTokenCache (token: OAuthTokenModel) { | 130 | static removeTokenCache (token: OAuthTokenModel) { |
131 | return clearCacheByToken(token.accessToken) | 131 | return clearCacheByToken(token.accessToken) |
132 | } | 132 | } |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 2ebe23ef1..b722bed14 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | import { ActorModel } from '../activitypub/actor' | 15 | import { ActorModel } from '../activitypub/actor' |
16 | import { getVideoSort, throwIfNotValid } from '../utils' | 16 | import { getVideoSort, throwIfNotValid } from '../utils' |
17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
18 | import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' | 18 | import { CONFIG, CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers' |
19 | import { VideoFileModel } from '../video/video-file' | 19 | import { VideoFileModel } from '../video/video-file' |
20 | import { getServerActor } from '../../helpers/utils' | 20 | import { getServerActor } from '../../helpers/utils' |
21 | import { VideoModel } from '../video/video' | 21 | import { VideoModel } from '../video/video' |
@@ -28,6 +28,7 @@ import { sample } from 'lodash' | |||
28 | import { isTestInstance } from '../../helpers/core-utils' | 28 | import { isTestInstance } from '../../helpers/core-utils' |
29 | import * as Bluebird from 'bluebird' | 29 | import * as Bluebird from 'bluebird' |
30 | import * as Sequelize from 'sequelize' | 30 | import * as Sequelize from 'sequelize' |
31 | import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' | ||
31 | 32 | ||
32 | export enum ScopeNames { | 33 | export enum ScopeNames { |
33 | WITH_VIDEO = 'WITH_VIDEO' | 34 | WITH_VIDEO = 'WITH_VIDEO' |
@@ -38,7 +39,17 @@ export enum ScopeNames { | |||
38 | include: [ | 39 | include: [ |
39 | { | 40 | { |
40 | model: () => VideoFileModel, | 41 | model: () => VideoFileModel, |
41 | required: true, | 42 | required: false, |
43 | include: [ | ||
44 | { | ||
45 | model: () => VideoModel, | ||
46 | required: true | ||
47 | } | ||
48 | ] | ||
49 | }, | ||
50 | { | ||
51 | model: () => VideoStreamingPlaylistModel, | ||
52 | required: false, | ||
42 | include: [ | 53 | include: [ |
43 | { | 54 | { |
44 | model: () => VideoModel, | 55 | model: () => VideoModel, |
@@ -97,12 +108,24 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
97 | 108 | ||
98 | @BelongsTo(() => VideoFileModel, { | 109 | @BelongsTo(() => VideoFileModel, { |
99 | foreignKey: { | 110 | foreignKey: { |
100 | allowNull: false | 111 | allowNull: true |
101 | }, | 112 | }, |
102 | onDelete: 'cascade' | 113 | onDelete: 'cascade' |
103 | }) | 114 | }) |
104 | VideoFile: VideoFileModel | 115 | VideoFile: VideoFileModel |
105 | 116 | ||
117 | @ForeignKey(() => VideoStreamingPlaylistModel) | ||
118 | @Column | ||
119 | videoStreamingPlaylistId: number | ||
120 | |||
121 | @BelongsTo(() => VideoStreamingPlaylistModel, { | ||
122 | foreignKey: { | ||
123 | allowNull: true | ||
124 | }, | ||
125 | onDelete: 'cascade' | ||
126 | }) | ||
127 | VideoStreamingPlaylist: VideoStreamingPlaylistModel | ||
128 | |||
106 | @ForeignKey(() => ActorModel) | 129 | @ForeignKey(() => ActorModel) |
107 | @Column | 130 | @Column |
108 | actorId: number | 131 | actorId: number |
@@ -117,16 +140,27 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
117 | 140 | ||
118 | @BeforeDestroy | 141 | @BeforeDestroy |
119 | static async removeFile (instance: VideoRedundancyModel) { | 142 | static async removeFile (instance: VideoRedundancyModel) { |
120 | // Not us | 143 | if (!instance.isOwned()) return |
121 | if (!instance.strategy) return | ||
122 | 144 | ||
123 | const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) | 145 | if (instance.videoFileId) { |
146 | const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) | ||
124 | 147 | ||
125 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 148 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
126 | logger.info('Removing duplicated video file %s.', logIdentifier) | 149 | logger.info('Removing duplicated video file %s.', logIdentifier) |
127 | 150 | ||
128 | videoFile.Video.removeFile(videoFile) | 151 | videoFile.Video.removeFile(videoFile, true) |
129 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 152 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
153 | } | ||
154 | |||
155 | if (instance.videoStreamingPlaylistId) { | ||
156 | const videoStreamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(instance.videoStreamingPlaylistId) | ||
157 | |||
158 | const videoUUID = videoStreamingPlaylist.Video.uuid | ||
159 | logger.info('Removing duplicated video streaming playlist %s.', videoUUID) | ||
160 | |||
161 | videoStreamingPlaylist.Video.removeStreamingPlaylist(true) | ||
162 | .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err })) | ||
163 | } | ||
130 | 164 | ||
131 | return undefined | 165 | return undefined |
132 | } | 166 | } |
@@ -144,6 +178,19 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
144 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | 178 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) |
145 | } | 179 | } |
146 | 180 | ||
181 | static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number) { | ||
182 | const actor = await getServerActor() | ||
183 | |||
184 | const query = { | ||
185 | where: { | ||
186 | actorId: actor.id, | ||
187 | videoStreamingPlaylistId | ||
188 | } | ||
189 | } | ||
190 | |||
191 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | ||
192 | } | ||
193 | |||
147 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { | 194 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { |
148 | const query = { | 195 | const query = { |
149 | where: { | 196 | where: { |
@@ -192,7 +239,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
192 | const ids = rows.map(r => r.id) | 239 | const ids = rows.map(r => r.id) |
193 | const id = sample(ids) | 240 | const id = sample(ids) |
194 | 241 | ||
195 | return VideoModel.loadWithFile(id, undefined, !isTestInstance()) | 242 | return VideoModel.loadWithFiles(id, undefined, !isTestInstance()) |
196 | } | 243 | } |
197 | 244 | ||
198 | static async findMostViewToDuplicate (randomizedFactor: number) { | 245 | static async findMostViewToDuplicate (randomizedFactor: number) { |
@@ -293,6 +340,11 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
293 | } | 340 | } |
294 | 341 | ||
295 | return VideoFileModel.sum('size', options as any) // FIXME: typings | 342 | return VideoFileModel.sum('size', options as any) // FIXME: typings |
343 | .then(v => { | ||
344 | if (!v || isNaN(v)) return 0 | ||
345 | |||
346 | return v | ||
347 | }) | ||
296 | } | 348 | } |
297 | 349 | ||
298 | static async listLocalExpired () { | 350 | static async listLocalExpired () { |
@@ -329,40 +381,44 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
329 | 381 | ||
330 | static async listLocalOfServer (serverId: number) { | 382 | static async listLocalOfServer (serverId: number) { |
331 | const actor = await getServerActor() | 383 | const actor = await getServerActor() |
332 | 384 | const buildVideoInclude = () => ({ | |
333 | const query = { | 385 | model: VideoModel, |
334 | where: { | 386 | required: true, |
335 | actorId: actor.id | ||
336 | }, | ||
337 | include: [ | 387 | include: [ |
338 | { | 388 | { |
339 | model: VideoFileModel, | 389 | attributes: [], |
390 | model: VideoChannelModel.unscoped(), | ||
340 | required: true, | 391 | required: true, |
341 | include: [ | 392 | include: [ |
342 | { | 393 | { |
343 | model: VideoModel, | 394 | attributes: [], |
395 | model: ActorModel.unscoped(), | ||
344 | required: true, | 396 | required: true, |
345 | include: [ | 397 | where: { |
346 | { | 398 | serverId |
347 | attributes: [], | 399 | } |
348 | model: VideoChannelModel.unscoped(), | ||
349 | required: true, | ||
350 | include: [ | ||
351 | { | ||
352 | attributes: [], | ||
353 | model: ActorModel.unscoped(), | ||
354 | required: true, | ||
355 | where: { | ||
356 | serverId | ||
357 | } | ||
358 | } | ||
359 | ] | ||
360 | } | ||
361 | ] | ||
362 | } | 400 | } |
363 | ] | 401 | ] |
364 | } | 402 | } |
365 | ] | 403 | ] |
404 | }) | ||
405 | |||
406 | const query = { | ||
407 | where: { | ||
408 | actorId: actor.id | ||
409 | }, | ||
410 | include: [ | ||
411 | { | ||
412 | model: VideoFileModel, | ||
413 | required: false, | ||
414 | include: [ buildVideoInclude() ] | ||
415 | }, | ||
416 | { | ||
417 | model: VideoStreamingPlaylistModel, | ||
418 | required: false, | ||
419 | include: [ buildVideoInclude() ] | ||
420 | } | ||
421 | ] | ||
366 | } | 422 | } |
367 | 423 | ||
368 | return VideoRedundancyModel.findAll(query) | 424 | return VideoRedundancyModel.findAll(query) |
@@ -391,7 +447,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
391 | ] | 447 | ] |
392 | } | 448 | } |
393 | 449 | ||
394 | return VideoRedundancyModel.find(query as any) // FIXME: typings | 450 | return VideoRedundancyModel.findOne(query as any) // FIXME: typings |
395 | .then((r: any) => ({ | 451 | .then((r: any) => ({ |
396 | totalUsed: parseInt(r.totalUsed.toString(), 10), | 452 | totalUsed: parseInt(r.totalUsed.toString(), 10), |
397 | totalVideos: r.totalVideos, | 453 | totalVideos: r.totalVideos, |
@@ -399,7 +455,32 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
399 | })) | 455 | })) |
400 | } | 456 | } |
401 | 457 | ||
458 | getVideo () { | ||
459 | if (this.VideoFile) return this.VideoFile.Video | ||
460 | |||
461 | return this.VideoStreamingPlaylist.Video | ||
462 | } | ||
463 | |||
464 | isOwned () { | ||
465 | return !!this.strategy | ||
466 | } | ||
467 | |||
402 | toActivityPubObject (): CacheFileObject { | 468 | toActivityPubObject (): CacheFileObject { |
469 | if (this.VideoStreamingPlaylist) { | ||
470 | return { | ||
471 | id: this.url, | ||
472 | type: 'CacheFile' as 'CacheFile', | ||
473 | object: this.VideoStreamingPlaylist.Video.url, | ||
474 | expires: this.expiresOn.toISOString(), | ||
475 | url: { | ||
476 | type: 'Link', | ||
477 | mimeType: 'application/x-mpegURL', | ||
478 | mediaType: 'application/x-mpegURL', | ||
479 | href: this.fileUrl | ||
480 | } | ||
481 | } | ||
482 | } | ||
483 | |||
403 | return { | 484 | return { |
404 | id: this.url, | 485 | id: this.url, |
405 | type: 'CacheFile' as 'CacheFile', | 486 | type: 'CacheFile' as 'CacheFile', |
@@ -407,7 +488,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
407 | expires: this.expiresOn.toISOString(), | 488 | expires: this.expiresOn.toISOString(), |
408 | url: { | 489 | url: { |
409 | type: 'Link', | 490 | type: 'Link', |
410 | mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, | 491 | mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, |
492 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, | ||
411 | href: this.fileUrl, | 493 | href: this.fileUrl, |
412 | height: this.VideoFile.resolution, | 494 | height: this.VideoFile.resolution, |
413 | size: this.VideoFile.size, | 495 | size: this.VideoFile.size, |
@@ -422,7 +504,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
422 | 504 | ||
423 | const notIn = Sequelize.literal( | 505 | const notIn = Sequelize.literal( |
424 | '(' + | 506 | '(' + |
425 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` + | 507 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` + |
426 | ')' | 508 | ')' |
427 | ) | 509 | ) |
428 | 510 | ||
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts new file mode 100644 index 000000000..450f27152 --- /dev/null +++ b/server/models/server/server-blocklist.ts | |||
@@ -0,0 +1,121 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { AccountModel } from '../account/account' | ||
3 | import { ServerModel } from './server' | ||
4 | import { ServerBlock } from '../../../shared/models/blocklist' | ||
5 | import { getSort } from '../utils' | ||
6 | |||
7 | enum ScopeNames { | ||
8 | WITH_ACCOUNT = 'WITH_ACCOUNT', | ||
9 | WITH_SERVER = 'WITH_SERVER' | ||
10 | } | ||
11 | |||
12 | @Scopes({ | ||
13 | [ScopeNames.WITH_ACCOUNT]: { | ||
14 | include: [ | ||
15 | { | ||
16 | model: () => AccountModel, | ||
17 | required: true | ||
18 | } | ||
19 | ] | ||
20 | }, | ||
21 | [ScopeNames.WITH_SERVER]: { | ||
22 | include: [ | ||
23 | { | ||
24 | model: () => ServerModel, | ||
25 | required: true | ||
26 | } | ||
27 | ] | ||
28 | } | ||
29 | }) | ||
30 | |||
31 | @Table({ | ||
32 | tableName: 'serverBlocklist', | ||
33 | indexes: [ | ||
34 | { | ||
35 | fields: [ 'accountId', 'targetServerId' ], | ||
36 | unique: true | ||
37 | }, | ||
38 | { | ||
39 | fields: [ 'targetServerId' ] | ||
40 | } | ||
41 | ] | ||
42 | }) | ||
43 | export class ServerBlocklistModel extends Model<ServerBlocklistModel> { | ||
44 | |||
45 | @CreatedAt | ||
46 | createdAt: Date | ||
47 | |||
48 | @UpdatedAt | ||
49 | updatedAt: Date | ||
50 | |||
51 | @ForeignKey(() => AccountModel) | ||
52 | @Column | ||
53 | accountId: number | ||
54 | |||
55 | @BelongsTo(() => AccountModel, { | ||
56 | foreignKey: { | ||
57 | name: 'accountId', | ||
58 | allowNull: false | ||
59 | }, | ||
60 | onDelete: 'CASCADE' | ||
61 | }) | ||
62 | ByAccount: AccountModel | ||
63 | |||
64 | @ForeignKey(() => ServerModel) | ||
65 | @Column | ||
66 | targetServerId: number | ||
67 | |||
68 | @BelongsTo(() => ServerModel, { | ||
69 | foreignKey: { | ||
70 | name: 'targetServerId', | ||
71 | allowNull: false | ||
72 | }, | ||
73 | onDelete: 'CASCADE' | ||
74 | }) | ||
75 | BlockedServer: ServerModel | ||
76 | |||
77 | static loadByAccountAndHost (accountId: number, host: string) { | ||
78 | const query = { | ||
79 | where: { | ||
80 | accountId | ||
81 | }, | ||
82 | include: [ | ||
83 | { | ||
84 | model: ServerModel, | ||
85 | where: { | ||
86 | host | ||
87 | }, | ||
88 | required: true | ||
89 | } | ||
90 | ] | ||
91 | } | ||
92 | |||
93 | return ServerBlocklistModel.findOne(query) | ||
94 | } | ||
95 | |||
96 | static listForApi (accountId: number, start: number, count: number, sort: string) { | ||
97 | const query = { | ||
98 | offset: start, | ||
99 | limit: count, | ||
100 | order: getSort(sort), | ||
101 | where: { | ||
102 | accountId | ||
103 | } | ||
104 | } | ||
105 | |||
106 | return ServerBlocklistModel | ||
107 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ]) | ||
108 | .findAndCountAll(query) | ||
109 | .then(({ rows, count }) => { | ||
110 | return { total: count, data: rows } | ||
111 | }) | ||
112 | } | ||
113 | |||
114 | toFormattedJSON (): ServerBlock { | ||
115 | return { | ||
116 | byAccount: this.ByAccount.toFormattedJSON(), | ||
117 | blockedServer: this.BlockedServer.toFormattedJSON(), | ||
118 | createdAt: this.createdAt | ||
119 | } | ||
120 | } | ||
121 | } | ||
diff --git a/server/models/server/server.ts b/server/models/server/server.ts index ca3b24d51..300d70938 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts | |||
@@ -49,4 +49,10 @@ export class ServerModel extends Model<ServerModel> { | |||
49 | 49 | ||
50 | return ServerModel.findOne(query) | 50 | return ServerModel.findOne(query) |
51 | } | 51 | } |
52 | |||
53 | toFormattedJSON () { | ||
54 | return { | ||
55 | host: this.host | ||
56 | } | ||
57 | } | ||
52 | } | 58 | } |
diff --git a/server/models/utils.ts b/server/models/utils.ts index e0bf091ad..5b4093aec 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -29,7 +29,11 @@ function getVideoSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { | |||
29 | ] | 29 | ] |
30 | } | 30 | } |
31 | 31 | ||
32 | return [ [ field, direction ], lastSort ] | 32 | const firstSort = typeof field === 'string' ? |
33 | field.split('.').concat([ direction ]) : | ||
34 | [ field, direction ] | ||
35 | |||
36 | return [ firstSort, lastSort ] | ||
33 | } | 37 | } |
34 | 38 | ||
35 | function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) { | 39 | function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) { |
@@ -64,9 +68,25 @@ function createSimilarityAttribute (col: string, value: string) { | |||
64 | ) | 68 | ) |
65 | } | 69 | } |
66 | 70 | ||
71 | function buildBlockedAccountSQL (serverAccountId: number, userAccountId?: number) { | ||
72 | const blockerIds = [ serverAccountId ] | ||
73 | if (userAccountId) blockerIds.push(userAccountId) | ||
74 | |||
75 | const blockerIdsString = blockerIds.join(', ') | ||
76 | |||
77 | const query = 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + | ||
78 | ' UNION ALL ' + | ||
79 | 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + | ||
80 | 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + | ||
81 | 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' | ||
82 | |||
83 | return query | ||
84 | } | ||
85 | |||
67 | // --------------------------------------------------------------------------- | 86 | // --------------------------------------------------------------------------- |
68 | 87 | ||
69 | export { | 88 | export { |
89 | buildBlockedAccountSQL, | ||
70 | SortType, | 90 | SortType, |
71 | getSort, | 91 | getSort, |
72 | getVideoSort, | 92 | getVideoSort, |
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index dbb88ca45..cc47644f2 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts | |||
@@ -1,17 +1,4 @@ | |||
1 | import { | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | AfterCreate, | ||
3 | AllowNull, | ||
4 | BelongsTo, | ||
5 | Column, | ||
6 | CreatedAt, | ||
7 | DataType, | ||
8 | Default, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | 2 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' |
16 | import { VideoAbuse } from '../../../shared/models/videos' | 3 | import { VideoAbuse } from '../../../shared/models/videos' |
17 | import { | 4 | import { |
@@ -19,7 +6,6 @@ import { | |||
19 | isVideoAbuseReasonValid, | 6 | isVideoAbuseReasonValid, |
20 | isVideoAbuseStateValid | 7 | isVideoAbuseStateValid |
21 | } from '../../helpers/custom-validators/video-abuses' | 8 | } from '../../helpers/custom-validators/video-abuses' |
22 | import { Emailer } from '../../lib/emailer' | ||
23 | import { AccountModel } from '../account/account' | 9 | import { AccountModel } from '../account/account' |
24 | import { getSort, throwIfNotValid } from '../utils' | 10 | import { getSort, throwIfNotValid } from '../utils' |
25 | import { VideoModel } from './video' | 11 | import { VideoModel } from './video' |
@@ -40,8 +26,9 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' | |||
40 | export class VideoAbuseModel extends Model<VideoAbuseModel> { | 26 | export class VideoAbuseModel extends Model<VideoAbuseModel> { |
41 | 27 | ||
42 | @AllowNull(false) | 28 | @AllowNull(false) |
29 | @Default(null) | ||
43 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) | 30 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) |
44 | @Column | 31 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) |
45 | reason: string | 32 | reason: string |
46 | 33 | ||
47 | @AllowNull(false) | 34 | @AllowNull(false) |
@@ -86,11 +73,6 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> { | |||
86 | }) | 73 | }) |
87 | Video: VideoModel | 74 | Video: VideoModel |
88 | 75 | ||
89 | @AfterCreate | ||
90 | static sendEmailNotification (instance: VideoAbuseModel) { | ||
91 | return Emailer.Instance.addVideoAbuseReportJob(instance.videoId) | ||
92 | } | ||
93 | |||
94 | static loadByIdAndVideoId (id: number, videoId: number) { | 76 | static loadByIdAndVideoId (id: number, videoId: number) { |
95 | const query = { | 77 | const query = { |
96 | where: { | 78 | where: { |
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 67f7cd487..3b567e488 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -1,21 +1,7 @@ | |||
1 | import { | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | AfterCreate, | ||
3 | AfterDestroy, | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' | 2 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' |
16 | import { VideoModel } from './video' | 3 | import { VideoModel } from './video' |
17 | import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' | 4 | import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' |
18 | import { Emailer } from '../../lib/emailer' | ||
19 | import { VideoBlacklist } from '../../../shared/models/videos' | 5 | import { VideoBlacklist } from '../../../shared/models/videos' |
20 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 6 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
21 | 7 | ||
@@ -35,6 +21,10 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
35 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) | 21 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) |
36 | reason: string | 22 | reason: string |
37 | 23 | ||
24 | @AllowNull(false) | ||
25 | @Column | ||
26 | unfederated: boolean | ||
27 | |||
38 | @CreatedAt | 28 | @CreatedAt |
39 | createdAt: Date | 29 | createdAt: Date |
40 | 30 | ||
@@ -53,16 +43,6 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
53 | }) | 43 | }) |
54 | Video: VideoModel | 44 | Video: VideoModel |
55 | 45 | ||
56 | @AfterCreate | ||
57 | static sendBlacklistEmailNotification (instance: VideoBlacklistModel) { | ||
58 | return Emailer.Instance.addVideoBlacklistReportJob(instance.videoId, instance.reason) | ||
59 | } | ||
60 | |||
61 | @AfterDestroy | ||
62 | static sendUnblacklistEmailNotification (instance: VideoBlacklistModel) { | ||
63 | return Emailer.Instance.addVideoUnblacklistReportJob(instance.videoId) | ||
64 | } | ||
65 | |||
66 | static listForApi (start: number, count: number, sort: SortType) { | 46 | static listForApi (start: number, count: number, sort: SortType) { |
67 | const query = { | 47 | const query = { |
68 | offset: start, | 48 | offset: start, |
@@ -103,6 +83,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
103 | createdAt: this.createdAt, | 83 | createdAt: this.createdAt, |
104 | updatedAt: this.updatedAt, | 84 | updatedAt: this.updatedAt, |
105 | reason: this.reason, | 85 | reason: this.reason, |
86 | unfederated: this.unfederated, | ||
106 | 87 | ||
107 | video: { | 88 | video: { |
108 | id: video.id, | 89 | id: video.id, |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index f4586917e..5598d80f6 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -233,6 +233,27 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
233 | }) | 233 | }) |
234 | } | 234 | } |
235 | 235 | ||
236 | static listLocalsForSitemap (sort: string) { | ||
237 | const query = { | ||
238 | attributes: [ ], | ||
239 | offset: 0, | ||
240 | order: getSort(sort), | ||
241 | include: [ | ||
242 | { | ||
243 | attributes: [ 'preferredUsername', 'serverId' ], | ||
244 | model: ActorModel.unscoped(), | ||
245 | where: { | ||
246 | serverId: null | ||
247 | } | ||
248 | } | ||
249 | ] | ||
250 | } | ||
251 | |||
252 | return VideoChannelModel | ||
253 | .unscoped() | ||
254 | .findAll(query) | ||
255 | } | ||
256 | |||
236 | static searchForApi (options: { | 257 | static searchForApi (options: { |
237 | actorId: number | 258 | actorId: number |
238 | search: string | 259 | search: string |
@@ -449,4 +470,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
449 | getDisplayName () { | 470 | getDisplayName () { |
450 | return this.name | 471 | return this.name |
451 | } | 472 | } |
473 | |||
474 | isOutdated () { | ||
475 | return this.Actor.isOutdated() | ||
476 | } | ||
452 | } | 477 | } |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index f84c1880c..cf6278da7 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,21 +1,37 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table, | 3 | AllowNull, |
4 | BeforeDestroy, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | ForeignKey, | ||
10 | IFindOptions, | ||
11 | Is, | ||
12 | Model, | ||
13 | Scopes, | ||
14 | Table, | ||
4 | UpdatedAt | 15 | UpdatedAt |
5 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
6 | import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' | 17 | import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' |
7 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 18 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
8 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' | 19 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 21 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' |
11 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' | 22 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' |
12 | import { AccountModel } from '../account/account' | 23 | import { AccountModel } from '../account/account' |
13 | import { ActorModel } from '../activitypub/actor' | 24 | import { ActorModel } from '../activitypub/actor' |
14 | import { AvatarModel } from '../avatar/avatar' | 25 | import { AvatarModel } from '../avatar/avatar' |
15 | import { ServerModel } from '../server/server' | 26 | import { ServerModel } from '../server/server' |
16 | import { getSort, throwIfNotValid } from '../utils' | 27 | import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils' |
17 | import { VideoModel } from './video' | 28 | import { VideoModel } from './video' |
18 | import { VideoChannelModel } from './video-channel' | 29 | import { VideoChannelModel } from './video-channel' |
30 | import { getServerActor } from '../../helpers/utils' | ||
31 | import { UserModel } from '../account/user' | ||
32 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' | ||
33 | import { regexpCapture } from '../../helpers/regexp' | ||
34 | import { uniq } from 'lodash' | ||
19 | 35 | ||
20 | enum ScopeNames { | 36 | enum ScopeNames { |
21 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 37 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
@@ -25,18 +41,29 @@ enum ScopeNames { | |||
25 | } | 41 | } |
26 | 42 | ||
27 | @Scopes({ | 43 | @Scopes({ |
28 | [ScopeNames.ATTRIBUTES_FOR_API]: { | 44 | [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => { |
29 | attributes: { | 45 | return { |
30 | include: [ | 46 | attributes: { |
31 | [ | 47 | include: [ |
32 | Sequelize.literal( | 48 | [ |
33 | '(SELECT COUNT("replies"."id") ' + | 49 | Sequelize.literal( |
34 | 'FROM "videoComment" AS "replies" ' + | 50 | '(' + |
35 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")' | 51 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' + |
36 | ), | 52 | 'SELECT COUNT("replies"."id") - (' + |
37 | 'totalReplies' | 53 | 'SELECT COUNT("replies"."id") ' + |
54 | 'FROM "videoComment" AS "replies" ' + | ||
55 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
56 | 'AND "accountId" IN (SELECT "id" FROM "blocklist")' + | ||
57 | ')' + | ||
58 | 'FROM "videoComment" AS "replies" ' + | ||
59 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
60 | 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' + | ||
61 | ')' | ||
62 | ), | ||
63 | 'totalReplies' | ||
64 | ] | ||
38 | ] | 65 | ] |
39 | ] | 66 | } |
40 | } | 67 | } |
41 | }, | 68 | }, |
42 | [ScopeNames.WITH_ACCOUNT]: { | 69 | [ScopeNames.WITH_ACCOUNT]: { |
@@ -267,26 +294,47 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
267 | return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) | 294 | return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) |
268 | } | 295 | } |
269 | 296 | ||
270 | static listThreadsForApi (videoId: number, start: number, count: number, sort: string) { | 297 | static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) { |
298 | const serverActor = await getServerActor() | ||
299 | const serverAccountId = serverActor.Account.id | ||
300 | const userAccountId = user ? user.Account.id : undefined | ||
301 | |||
271 | const query = { | 302 | const query = { |
272 | offset: start, | 303 | offset: start, |
273 | limit: count, | 304 | limit: count, |
274 | order: getSort(sort), | 305 | order: getSort(sort), |
275 | where: { | 306 | where: { |
276 | videoId, | 307 | videoId, |
277 | inReplyToCommentId: null | 308 | inReplyToCommentId: null, |
309 | accountId: { | ||
310 | [Sequelize.Op.notIn]: Sequelize.literal( | ||
311 | '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' | ||
312 | ) | ||
313 | } | ||
278 | } | 314 | } |
279 | } | 315 | } |
280 | 316 | ||
317 | // FIXME: typings | ||
318 | const scopes: any[] = [ | ||
319 | ScopeNames.WITH_ACCOUNT, | ||
320 | { | ||
321 | method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] | ||
322 | } | ||
323 | ] | ||
324 | |||
281 | return VideoCommentModel | 325 | return VideoCommentModel |
282 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) | 326 | .scope(scopes) |
283 | .findAndCountAll(query) | 327 | .findAndCountAll(query) |
284 | .then(({ rows, count }) => { | 328 | .then(({ rows, count }) => { |
285 | return { total: count, data: rows } | 329 | return { total: count, data: rows } |
286 | }) | 330 | }) |
287 | } | 331 | } |
288 | 332 | ||
289 | static listThreadCommentsForApi (videoId: number, threadId: number) { | 333 | static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) { |
334 | const serverActor = await getServerActor() | ||
335 | const serverAccountId = serverActor.Account.id | ||
336 | const userAccountId = user ? user.Account.id : undefined | ||
337 | |||
290 | const query = { | 338 | const query = { |
291 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], | 339 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], |
292 | where: { | 340 | where: { |
@@ -294,12 +342,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
294 | [ Sequelize.Op.or ]: [ | 342 | [ Sequelize.Op.or ]: [ |
295 | { id: threadId }, | 343 | { id: threadId }, |
296 | { originCommentId: threadId } | 344 | { originCommentId: threadId } |
297 | ] | 345 | ], |
346 | accountId: { | ||
347 | [Sequelize.Op.notIn]: Sequelize.literal( | ||
348 | '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' | ||
349 | ) | ||
350 | } | ||
298 | } | 351 | } |
299 | } | 352 | } |
300 | 353 | ||
354 | const scopes: any[] = [ | ||
355 | ScopeNames.WITH_ACCOUNT, | ||
356 | { | ||
357 | method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] | ||
358 | } | ||
359 | ] | ||
360 | |||
301 | return VideoCommentModel | 361 | return VideoCommentModel |
302 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) | 362 | .scope(scopes) |
303 | .findAndCountAll(query) | 363 | .findAndCountAll(query) |
304 | .then(({ rows, count }) => { | 364 | .then(({ rows, count }) => { |
305 | return { total: count, data: rows } | 365 | return { total: count, data: rows } |
@@ -313,9 +373,11 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
313 | id: { | 373 | id: { |
314 | [ Sequelize.Op.in ]: Sequelize.literal('(' + | 374 | [ Sequelize.Op.in ]: Sequelize.literal('(' + |
315 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + | 375 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + |
316 | 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' + | 376 | `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + |
317 | 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' + | 377 | 'UNION ' + |
318 | 'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' + | 378 | 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' + |
379 | 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' + | ||
380 | ') ' + | ||
319 | 'SELECT id FROM children' + | 381 | 'SELECT id FROM children' + |
320 | ')'), | 382 | ')'), |
321 | [ Sequelize.Op.ne ]: comment.id | 383 | [ Sequelize.Op.ne ]: comment.id |
@@ -391,6 +453,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
391 | } | 453 | } |
392 | } | 454 | } |
393 | 455 | ||
456 | getCommentStaticPath () { | ||
457 | return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId() | ||
458 | } | ||
459 | |||
394 | getThreadId (): number { | 460 | getThreadId (): number { |
395 | return this.originCommentId || this.id | 461 | return this.originCommentId || this.id |
396 | } | 462 | } |
@@ -399,6 +465,34 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
399 | return this.Account.isOwned() | 465 | return this.Account.isOwned() |
400 | } | 466 | } |
401 | 467 | ||
468 | extractMentions () { | ||
469 | if (!this.text) return [] | ||
470 | |||
471 | const localMention = `@(${actorNameAlphabet}+)` | ||
472 | const remoteMention = `${localMention}@${CONFIG.WEBSERVER.HOST}` | ||
473 | |||
474 | const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g') | ||
475 | const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g') | ||
476 | const firstMentionRegex = new RegExp('^(?:(?:' + remoteMention + ')|(?:' + localMention + ')) ', 'g') | ||
477 | const endMentionRegex = new RegExp(' (?:(?:' + remoteMention + ')|(?:' + localMention + '))$', 'g') | ||
478 | |||
479 | return uniq( | ||
480 | [].concat( | ||
481 | regexpCapture(this.text, remoteMentionsRegex) | ||
482 | .map(([ , username ]) => username), | ||
483 | |||
484 | regexpCapture(this.text, localMentionsRegex) | ||
485 | .map(([ , username ]) => username), | ||
486 | |||
487 | regexpCapture(this.text, firstMentionRegex) | ||
488 | .map(([ , username1, username2 ]) => username1 || username2), | ||
489 | |||
490 | regexpCapture(this.text, endMentionRegex) | ||
491 | .map(([ , username1, username2 ]) => username1 || username2) | ||
492 | ) | ||
493 | ) | ||
494 | } | ||
495 | |||
402 | toFormattedJSON () { | 496 | toFormattedJSON () { |
403 | return { | 497 | return { |
404 | id: this.id, | 498 | id: this.id, |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index adebdf0c7..7d1e371b9 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | import { values } from 'lodash' | ||
2 | import { | 1 | import { |
3 | AllowNull, | 2 | AllowNull, |
4 | BelongsTo, | 3 | BelongsTo, |
@@ -14,12 +13,12 @@ import { | |||
14 | UpdatedAt | 13 | UpdatedAt |
15 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
16 | import { | 15 | import { |
16 | isVideoFileExtnameValid, | ||
17 | isVideoFileInfoHashValid, | 17 | isVideoFileInfoHashValid, |
18 | isVideoFileResolutionValid, | 18 | isVideoFileResolutionValid, |
19 | isVideoFileSizeValid, | 19 | isVideoFileSizeValid, |
20 | isVideoFPSResolutionValid | 20 | isVideoFPSResolutionValid |
21 | } from '../../helpers/custom-validators/videos' | 21 | } from '../../helpers/custom-validators/videos' |
22 | import { CONSTRAINTS_FIELDS } from '../../initializers' | ||
23 | import { throwIfNotValid } from '../utils' | 22 | import { throwIfNotValid } from '../utils' |
24 | import { VideoModel } from './video' | 23 | import { VideoModel } from './video' |
25 | import * as Sequelize from 'sequelize' | 24 | import * as Sequelize from 'sequelize' |
@@ -58,11 +57,12 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
58 | size: number | 57 | size: number |
59 | 58 | ||
60 | @AllowNull(false) | 59 | @AllowNull(false) |
61 | @Column(DataType.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME))) | 60 | @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname')) |
61 | @Column | ||
62 | extname: string | 62 | extname: string |
63 | 63 | ||
64 | @AllowNull(false) | 64 | @AllowNull(false) |
65 | @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash')) | 65 | @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash')) |
66 | @Column | 66 | @Column |
67 | infoHash: string | 67 | infoHash: string |
68 | 68 | ||
@@ -86,14 +86,14 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
86 | 86 | ||
87 | @HasMany(() => VideoRedundancyModel, { | 87 | @HasMany(() => VideoRedundancyModel, { |
88 | foreignKey: { | 88 | foreignKey: { |
89 | allowNull: false | 89 | allowNull: true |
90 | }, | 90 | }, |
91 | onDelete: 'CASCADE', | 91 | onDelete: 'CASCADE', |
92 | hooks: true | 92 | hooks: true |
93 | }) | 93 | }) |
94 | RedundancyVideos: VideoRedundancyModel[] | 94 | RedundancyVideos: VideoRedundancyModel[] |
95 | 95 | ||
96 | static isInfohashExists (infoHash: string) { | 96 | static doesInfohashExist (infoHash: string) { |
97 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' | 97 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' |
98 | const options = { | 98 | const options = { |
99 | type: Sequelize.QueryTypes.SELECT, | 99 | type: Sequelize.QueryTypes.SELECT, |
@@ -120,6 +120,26 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
120 | return VideoFileModel.findById(id, options) | 120 | return VideoFileModel.findById(id, options) |
121 | } | 121 | } |
122 | 122 | ||
123 | static async getStats () { | ||
124 | let totalLocalVideoFilesSize = await VideoFileModel.sum('size', { | ||
125 | include: [ | ||
126 | { | ||
127 | attributes: [], | ||
128 | model: VideoModel.unscoped(), | ||
129 | where: { | ||
130 | remote: false | ||
131 | } | ||
132 | } | ||
133 | ] | ||
134 | } as any) | ||
135 | // Sequelize could return null... | ||
136 | if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0 | ||
137 | |||
138 | return { | ||
139 | totalLocalVideoFilesSize | ||
140 | } | ||
141 | } | ||
142 | |||
123 | hasSameUniqueKeysThan (other: VideoFileModel) { | 143 | hasSameUniqueKeysThan (other: VideoFileModel) { |
124 | return this.fps === other.fps && | 144 | return this.fps === other.fps && |
125 | this.resolution === other.resolution && | 145 | this.resolution === other.resolution && |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index e7bff2ed7..76d0445d4 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -1,8 +1,13 @@ | |||
1 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 1 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' |
2 | import { VideoModel } from './video' | 2 | import { VideoModel } from './video' |
3 | import { VideoFileModel } from './video-file' | 3 | import { VideoFileModel } from './video-file' |
4 | import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 4 | import { |
5 | import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' | 5 | ActivityPlaylistInfohashesObject, |
6 | ActivityPlaylistSegmentHashesObject, | ||
7 | ActivityUrlObject, | ||
8 | VideoTorrentObject | ||
9 | } from '../../../shared/models/activitypub/objects' | ||
10 | import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' | ||
6 | import { VideoCaptionModel } from './video-caption' | 11 | import { VideoCaptionModel } from './video-caption' |
7 | import { | 12 | import { |
8 | getVideoCommentsActivityPubUrl, | 13 | getVideoCommentsActivityPubUrl, |
@@ -11,6 +16,8 @@ import { | |||
11 | getVideoSharesActivityPubUrl | 16 | getVideoSharesActivityPubUrl |
12 | } from '../../lib/activitypub' | 17 | } from '../../lib/activitypub' |
13 | import { isArray } from '../../helpers/custom-validators/misc' | 18 | import { isArray } from '../../helpers/custom-validators/misc' |
19 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | ||
20 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | ||
14 | 21 | ||
15 | export type VideoFormattingJSONOptions = { | 22 | export type VideoFormattingJSONOptions = { |
16 | completeDescription?: boolean | 23 | completeDescription?: boolean |
@@ -120,7 +127,12 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | |||
120 | } | 127 | } |
121 | }) | 128 | }) |
122 | 129 | ||
130 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | ||
131 | |||
123 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] | 132 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] |
133 | |||
134 | const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
135 | |||
124 | const detailsJson = { | 136 | const detailsJson = { |
125 | support: video.support, | 137 | support: video.support, |
126 | descriptionPath: video.getDescriptionAPIPath(), | 138 | descriptionPath: video.getDescriptionAPIPath(), |
@@ -134,7 +146,11 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | |||
134 | id: video.state, | 146 | id: video.state, |
135 | label: VideoModel.getStateLabel(video.state) | 147 | label: VideoModel.getStateLabel(video.state) |
136 | }, | 148 | }, |
137 | files: [] | 149 | |
150 | trackerUrls: video.getTrackerUrls(baseUrlHttp, baseUrlWs), | ||
151 | |||
152 | files: [], | ||
153 | streamingPlaylists | ||
138 | } | 154 | } |
139 | 155 | ||
140 | // Format and sort video files | 156 | // Format and sort video files |
@@ -143,6 +159,25 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | |||
143 | return Object.assign(formattedJson, detailsJson) | 159 | return Object.assign(formattedJson, detailsJson) |
144 | } | 160 | } |
145 | 161 | ||
162 | function streamingPlaylistsModelToFormattedJSON (video: VideoModel, playlists: VideoStreamingPlaylistModel[]): VideoStreamingPlaylist[] { | ||
163 | if (isArray(playlists) === false) return [] | ||
164 | |||
165 | return playlists | ||
166 | .map(playlist => { | ||
167 | const redundancies = isArray(playlist.RedundancyVideos) | ||
168 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
169 | : [] | ||
170 | |||
171 | return { | ||
172 | id: playlist.id, | ||
173 | type: playlist.type, | ||
174 | playlistUrl: playlist.playlistUrl, | ||
175 | segmentsSha256Url: playlist.segmentsSha256Url, | ||
176 | redundancies | ||
177 | } as VideoStreamingPlaylist | ||
178 | }) | ||
179 | } | ||
180 | |||
146 | function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { | 181 | function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { |
147 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 182 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() |
148 | 183 | ||
@@ -208,7 +243,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
208 | for (const file of video.VideoFiles) { | 243 | for (const file of video.VideoFiles) { |
209 | url.push({ | 244 | url.push({ |
210 | type: 'Link', | 245 | type: 'Link', |
211 | mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | 246 | mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, |
247 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, | ||
212 | href: video.getVideoFileUrl(file, baseUrlHttp), | 248 | href: video.getVideoFileUrl(file, baseUrlHttp), |
213 | height: file.resolution, | 249 | height: file.resolution, |
214 | size: file.size, | 250 | size: file.size, |
@@ -218,6 +254,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
218 | url.push({ | 254 | url.push({ |
219 | type: 'Link', | 255 | type: 'Link', |
220 | mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', | 256 | mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', |
257 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
221 | href: video.getTorrentUrl(file, baseUrlHttp), | 258 | href: video.getTorrentUrl(file, baseUrlHttp), |
222 | height: file.resolution | 259 | height: file.resolution |
223 | }) | 260 | }) |
@@ -225,15 +262,39 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
225 | url.push({ | 262 | url.push({ |
226 | type: 'Link', | 263 | type: 'Link', |
227 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | 264 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', |
265 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
228 | href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs), | 266 | href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs), |
229 | height: file.resolution | 267 | height: file.resolution |
230 | }) | 268 | }) |
231 | } | 269 | } |
232 | 270 | ||
271 | for (const playlist of (video.VideoStreamingPlaylists || [])) { | ||
272 | let tag: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[] | ||
273 | |||
274 | tag = playlist.p2pMediaLoaderInfohashes | ||
275 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) | ||
276 | tag.push({ | ||
277 | type: 'Link', | ||
278 | name: 'sha256', | ||
279 | mimeType: 'application/json' as 'application/json', | ||
280 | mediaType: 'application/json' as 'application/json', | ||
281 | href: playlist.segmentsSha256Url | ||
282 | }) | ||
283 | |||
284 | url.push({ | ||
285 | type: 'Link', | ||
286 | mimeType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
287 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
288 | href: playlist.playlistUrl, | ||
289 | tag | ||
290 | }) | ||
291 | } | ||
292 | |||
233 | // Add video url too | 293 | // Add video url too |
234 | url.push({ | 294 | url.push({ |
235 | type: 'Link', | 295 | type: 'Link', |
236 | mimeType: 'text/html', | 296 | mimeType: 'text/html', |
297 | mediaType: 'text/html', | ||
237 | href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | 298 | href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid |
238 | }) | 299 | }) |
239 | 300 | ||
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 8d442b3f8..c723e57c0 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -144,6 +144,10 @@ export class VideoImportModel extends Model<VideoImportModel> { | |||
144 | }) | 144 | }) |
145 | } | 145 | } |
146 | 146 | ||
147 | getTargetIdentifier () { | ||
148 | return this.targetUrl || this.magnetUri || this.torrentName | ||
149 | } | ||
150 | |||
147 | toFormattedJSON (): VideoImport { | 151 | toFormattedJSON (): VideoImport { |
148 | const videoFormatOptions = { | 152 | const videoFormatOptions = { |
149 | completeDescription: true, | 153 | completeDescription: true, |
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index fa9a70d50..c87f71277 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -88,7 +88,7 @@ export class VideoShareModel extends Model<VideoShareModel> { | |||
88 | }) | 88 | }) |
89 | Video: VideoModel | 89 | Video: VideoModel |
90 | 90 | ||
91 | static load (actorId: number, videoId: number, t: Sequelize.Transaction) { | 91 | static load (actorId: number, videoId: number, t?: Sequelize.Transaction) { |
92 | return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({ | 92 | return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({ |
93 | where: { | 93 | where: { |
94 | actorId, | 94 | actorId, |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts new file mode 100644 index 000000000..bf6f7b0c4 --- /dev/null +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -0,0 +1,158 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | ||
3 | import { throwIfNotValid } from '../utils' | ||
4 | import { VideoModel } from './video' | ||
5 | import * as Sequelize from 'sequelize' | ||
6 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | ||
7 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
8 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
9 | import { CONSTRAINTS_FIELDS, STATIC_PATHS } from '../../initializers' | ||
10 | import { VideoFileModel } from './video-file' | ||
11 | import { join } from 'path' | ||
12 | import { sha1 } from '../../helpers/core-utils' | ||
13 | import { isArrayOf } from '../../helpers/custom-validators/misc' | ||
14 | |||
15 | @Table({ | ||
16 | tableName: 'videoStreamingPlaylist', | ||
17 | indexes: [ | ||
18 | { | ||
19 | fields: [ 'videoId' ] | ||
20 | }, | ||
21 | { | ||
22 | fields: [ 'videoId', 'type' ], | ||
23 | unique: true | ||
24 | }, | ||
25 | { | ||
26 | fields: [ 'p2pMediaLoaderInfohashes' ], | ||
27 | using: 'gin' | ||
28 | } | ||
29 | ] | ||
30 | }) | ||
31 | export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> { | ||
32 | @CreatedAt | ||
33 | createdAt: Date | ||
34 | |||
35 | @UpdatedAt | ||
36 | updatedAt: Date | ||
37 | |||
38 | @AllowNull(false) | ||
39 | @Column | ||
40 | type: VideoStreamingPlaylistType | ||
41 | |||
42 | @AllowNull(false) | ||
43 | @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url')) | ||
44 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) | ||
45 | playlistUrl: string | ||
46 | |||
47 | @AllowNull(false) | ||
48 | @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes')) | ||
49 | @Column(DataType.ARRAY(DataType.STRING)) | ||
50 | p2pMediaLoaderInfohashes: string[] | ||
51 | |||
52 | @AllowNull(false) | ||
53 | @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url')) | ||
54 | @Column | ||
55 | segmentsSha256Url: string | ||
56 | |||
57 | @ForeignKey(() => VideoModel) | ||
58 | @Column | ||
59 | videoId: number | ||
60 | |||
61 | @BelongsTo(() => VideoModel, { | ||
62 | foreignKey: { | ||
63 | allowNull: false | ||
64 | }, | ||
65 | onDelete: 'CASCADE' | ||
66 | }) | ||
67 | Video: VideoModel | ||
68 | |||
69 | @HasMany(() => VideoRedundancyModel, { | ||
70 | foreignKey: { | ||
71 | allowNull: false | ||
72 | }, | ||
73 | onDelete: 'CASCADE', | ||
74 | hooks: true | ||
75 | }) | ||
76 | RedundancyVideos: VideoRedundancyModel[] | ||
77 | |||
78 | static doesInfohashExist (infoHash: string) { | ||
79 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' | ||
80 | const options = { | ||
81 | type: Sequelize.QueryTypes.SELECT, | ||
82 | bind: { infoHash }, | ||
83 | raw: true | ||
84 | } | ||
85 | |||
86 | return VideoModel.sequelize.query(query, options) | ||
87 | .then(results => { | ||
88 | return results.length === 1 | ||
89 | }) | ||
90 | } | ||
91 | |||
92 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: VideoFileModel[]) { | ||
93 | const hashes: string[] = [] | ||
94 | |||
95 | // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L97 | ||
96 | for (let i = 0; i < videoFiles.length; i++) { | ||
97 | hashes.push(sha1(`1${playlistUrl}+V${i}`)) | ||
98 | } | ||
99 | |||
100 | return hashes | ||
101 | } | ||
102 | |||
103 | static loadWithVideo (id: number) { | ||
104 | const options = { | ||
105 | include: [ | ||
106 | { | ||
107 | model: VideoModel.unscoped(), | ||
108 | required: true | ||
109 | } | ||
110 | ] | ||
111 | } | ||
112 | |||
113 | return VideoStreamingPlaylistModel.findById(id, options) | ||
114 | } | ||
115 | |||
116 | static getHlsPlaylistFilename (resolution: number) { | ||
117 | return resolution + '.m3u8' | ||
118 | } | ||
119 | |||
120 | static getMasterHlsPlaylistFilename () { | ||
121 | return 'master.m3u8' | ||
122 | } | ||
123 | |||
124 | static getHlsSha256SegmentsFilename () { | ||
125 | return 'segments-sha256.json' | ||
126 | } | ||
127 | |||
128 | static getHlsVideoName (uuid: string, resolution: number) { | ||
129 | return `${uuid}-${resolution}-fragmented.mp4` | ||
130 | } | ||
131 | |||
132 | static getHlsMasterPlaylistStaticPath (videoUUID: string) { | ||
133 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | ||
134 | } | ||
135 | |||
136 | static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) { | ||
137 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | ||
138 | } | ||
139 | |||
140 | static getHlsSha256SegmentsStaticPath (videoUUID: string) { | ||
141 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | ||
142 | } | ||
143 | |||
144 | getStringType () { | ||
145 | if (this.type === VideoStreamingPlaylistType.HLS) return 'hls' | ||
146 | |||
147 | return 'unknown' | ||
148 | } | ||
149 | |||
150 | getVideoRedundancyUrl (baseUrlHttp: string) { | ||
151 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid | ||
152 | } | ||
153 | |||
154 | hasSameUniqueKeysThan (other: VideoStreamingPlaylistModel) { | ||
155 | return this.type === other.type && | ||
156 | this.videoId === other.videoId | ||
157 | } | ||
158 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index a9baaf1da..0feeed4f8 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -27,7 +27,7 @@ import { | |||
27 | Table, | 27 | Table, |
28 | UpdatedAt | 28 | UpdatedAt |
29 | } from 'sequelize-typescript' | 29 | } from 'sequelize-typescript' |
30 | import { VideoPrivacy, VideoState } from '../../../shared' | 30 | import { UserRight, VideoPrivacy, VideoState } from '../../../shared' |
31 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 31 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
32 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 32 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' |
33 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' | 33 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' |
@@ -52,7 +52,7 @@ import { | |||
52 | ACTIVITY_PUB, | 52 | ACTIVITY_PUB, |
53 | API_VERSION, | 53 | API_VERSION, |
54 | CONFIG, | 54 | CONFIG, |
55 | CONSTRAINTS_FIELDS, | 55 | CONSTRAINTS_FIELDS, HLS_PLAYLIST_DIRECTORY, HLS_REDUNDANCY_DIRECTORY, |
56 | PREVIEWS_SIZE, | 56 | PREVIEWS_SIZE, |
57 | REMOTE_SCHEME, | 57 | REMOTE_SCHEME, |
58 | STATIC_DOWNLOAD_PATHS, | 58 | STATIC_DOWNLOAD_PATHS, |
@@ -70,7 +70,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate' | |||
70 | import { ActorModel } from '../activitypub/actor' | 70 | import { ActorModel } from '../activitypub/actor' |
71 | import { AvatarModel } from '../avatar/avatar' | 71 | import { AvatarModel } from '../avatar/avatar' |
72 | import { ServerModel } from '../server/server' | 72 | import { ServerModel } from '../server/server' |
73 | import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' | 73 | import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' |
74 | import { TagModel } from './tag' | 74 | import { TagModel } from './tag' |
75 | import { VideoAbuseModel } from './video-abuse' | 75 | import { VideoAbuseModel } from './video-abuse' |
76 | import { VideoChannelModel } from './video-channel' | 76 | import { VideoChannelModel } from './video-channel' |
@@ -93,6 +93,9 @@ import { | |||
93 | } from './video-format-utils' | 93 | } from './video-format-utils' |
94 | import * as validator from 'validator' | 94 | import * as validator from 'validator' |
95 | import { UserVideoHistoryModel } from '../account/user-video-history' | 95 | import { UserVideoHistoryModel } from '../account/user-video-history' |
96 | import { UserModel } from '../account/user' | ||
97 | import { VideoImportModel } from './video-import' | ||
98 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | ||
96 | 99 | ||
97 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 100 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
98 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 101 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -101,17 +104,45 @@ const indexes: Sequelize.DefineIndexesOptions[] = [ | |||
101 | { fields: [ 'createdAt' ] }, | 104 | { fields: [ 'createdAt' ] }, |
102 | { fields: [ 'publishedAt' ] }, | 105 | { fields: [ 'publishedAt' ] }, |
103 | { fields: [ 'duration' ] }, | 106 | { fields: [ 'duration' ] }, |
104 | { fields: [ 'category' ] }, | ||
105 | { fields: [ 'licence' ] }, | ||
106 | { fields: [ 'nsfw' ] }, | ||
107 | { fields: [ 'language' ] }, | ||
108 | { fields: [ 'waitTranscoding' ] }, | ||
109 | { fields: [ 'state' ] }, | ||
110 | { fields: [ 'remote' ] }, | ||
111 | { fields: [ 'views' ] }, | 107 | { fields: [ 'views' ] }, |
112 | { fields: [ 'likes' ] }, | ||
113 | { fields: [ 'channelId' ] }, | 108 | { fields: [ 'channelId' ] }, |
114 | { | 109 | { |
110 | fields: [ 'category' ], // We don't care videos with an unknown category | ||
111 | where: { | ||
112 | category: { | ||
113 | [Sequelize.Op.ne]: null | ||
114 | } | ||
115 | } | ||
116 | }, | ||
117 | { | ||
118 | fields: [ 'licence' ], // We don't care videos with an unknown licence | ||
119 | where: { | ||
120 | licence: { | ||
121 | [Sequelize.Op.ne]: null | ||
122 | } | ||
123 | } | ||
124 | }, | ||
125 | { | ||
126 | fields: [ 'language' ], // We don't care videos with an unknown language | ||
127 | where: { | ||
128 | language: { | ||
129 | [Sequelize.Op.ne]: null | ||
130 | } | ||
131 | } | ||
132 | }, | ||
133 | { | ||
134 | fields: [ 'nsfw' ], // Most of the videos are not NSFW | ||
135 | where: { | ||
136 | nsfw: true | ||
137 | } | ||
138 | }, | ||
139 | { | ||
140 | fields: [ 'remote' ], // Only index local videos | ||
141 | where: { | ||
142 | remote: false | ||
143 | } | ||
144 | }, | ||
145 | { | ||
115 | fields: [ 'uuid' ], | 146 | fields: [ 'uuid' ], |
116 | unique: true | 147 | unique: true |
117 | }, | 148 | }, |
@@ -129,7 +160,9 @@ export enum ScopeNames { | |||
129 | WITH_FILES = 'WITH_FILES', | 160 | WITH_FILES = 'WITH_FILES', |
130 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 161 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
131 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 162 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
132 | WITH_USER_HISTORY = 'WITH_USER_HISTORY' | 163 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', |
164 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | ||
165 | WITH_USER_ID = 'WITH_USER_ID' | ||
133 | } | 166 | } |
134 | 167 | ||
135 | type ForAPIOptions = { | 168 | type ForAPIOptions = { |
@@ -138,7 +171,8 @@ type ForAPIOptions = { | |||
138 | } | 171 | } |
139 | 172 | ||
140 | type AvailableForListIDsOptions = { | 173 | type AvailableForListIDsOptions = { |
141 | actorId: number | 174 | serverAccountId: number |
175 | followerActorId: number | ||
142 | includeLocalVideos: boolean | 176 | includeLocalVideos: boolean |
143 | filter?: VideoFilter | 177 | filter?: VideoFilter |
144 | categoryOneOf?: number[] | 178 | categoryOneOf?: number[] |
@@ -151,6 +185,8 @@ type AvailableForListIDsOptions = { | |||
151 | accountId?: number | 185 | accountId?: number |
152 | videoChannelId?: number | 186 | videoChannelId?: number |
153 | trendingDays?: number | 187 | trendingDays?: number |
188 | user?: UserModel, | ||
189 | historyOfUser?: UserModel | ||
154 | } | 190 | } |
155 | 191 | ||
156 | @Scopes({ | 192 | @Scopes({ |
@@ -236,6 +272,22 @@ type AvailableForListIDsOptions = { | |||
236 | } | 272 | } |
237 | ] | 273 | ] |
238 | }, | 274 | }, |
275 | channelId: { | ||
276 | [ Sequelize.Op.notIn ]: Sequelize.literal( | ||
277 | '(' + | ||
278 | 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + | ||
279 | buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + | ||
280 | ')' + | ||
281 | ')' | ||
282 | ) | ||
283 | } | ||
284 | }, | ||
285 | include: [] | ||
286 | } | ||
287 | |||
288 | // Only list public/published videos | ||
289 | if (!options.filter || options.filter !== 'all-local') { | ||
290 | const privacyWhere = { | ||
239 | // Always list public videos | 291 | // Always list public videos |
240 | privacy: VideoPrivacy.PUBLIC, | 292 | privacy: VideoPrivacy.PUBLIC, |
241 | // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding | 293 | // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding |
@@ -250,8 +302,9 @@ type AvailableForListIDsOptions = { | |||
250 | } | 302 | } |
251 | } | 303 | } |
252 | ] | 304 | ] |
253 | }, | 305 | } |
254 | include: [] | 306 | |
307 | Object.assign(query.where, privacyWhere) | ||
255 | } | 308 | } |
256 | 309 | ||
257 | if (options.filter || options.accountId || options.videoChannelId) { | 310 | if (options.filter || options.accountId || options.videoChannelId) { |
@@ -295,7 +348,7 @@ type AvailableForListIDsOptions = { | |||
295 | query.include.push(videoChannelInclude) | 348 | query.include.push(videoChannelInclude) |
296 | } | 349 | } |
297 | 350 | ||
298 | if (options.actorId) { | 351 | if (options.followerActorId) { |
299 | let localVideosReq = '' | 352 | let localVideosReq = '' |
300 | if (options.includeLocalVideos === true) { | 353 | if (options.includeLocalVideos === true) { |
301 | localVideosReq = ' UNION ALL ' + | 354 | localVideosReq = ' UNION ALL ' + |
@@ -307,7 +360,7 @@ type AvailableForListIDsOptions = { | |||
307 | } | 360 | } |
308 | 361 | ||
309 | // Force actorId to be a number to avoid SQL injections | 362 | // Force actorId to be a number to avoid SQL injections |
310 | const actorIdNumber = parseInt(options.actorId.toString(), 10) | 363 | const actorIdNumber = parseInt(options.followerActorId.toString(), 10) |
311 | query.where[ 'id' ][ Sequelize.Op.and ].push({ | 364 | query.where[ 'id' ][ Sequelize.Op.and ].push({ |
312 | [ Sequelize.Op.in ]: Sequelize.literal( | 365 | [ Sequelize.Op.in ]: Sequelize.literal( |
313 | '(' + | 366 | '(' + |
@@ -396,8 +449,39 @@ type AvailableForListIDsOptions = { | |||
396 | query.subQuery = false | 449 | query.subQuery = false |
397 | } | 450 | } |
398 | 451 | ||
452 | if (options.historyOfUser) { | ||
453 | query.include.push({ | ||
454 | model: UserVideoHistoryModel, | ||
455 | required: true, | ||
456 | where: { | ||
457 | userId: options.historyOfUser.id | ||
458 | } | ||
459 | }) | ||
460 | |||
461 | // Even if the relation is n:m, we know that a user only have 0..1 video history | ||
462 | // So we won't have multiple rows for the same video | ||
463 | // Without this, we would not be able to sort on "updatedAt" column of UserVideoHistoryModel | ||
464 | query.subQuery = false | ||
465 | } | ||
466 | |||
399 | return query | 467 | return query |
400 | }, | 468 | }, |
469 | [ ScopeNames.WITH_USER_ID ]: { | ||
470 | include: [ | ||
471 | { | ||
472 | attributes: [ 'accountId' ], | ||
473 | model: () => VideoChannelModel.unscoped(), | ||
474 | required: true, | ||
475 | include: [ | ||
476 | { | ||
477 | attributes: [ 'userId' ], | ||
478 | model: () => AccountModel.unscoped(), | ||
479 | required: true | ||
480 | } | ||
481 | ] | ||
482 | } | ||
483 | ] | ||
484 | }, | ||
401 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { | 485 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { |
402 | include: [ | 486 | include: [ |
403 | { | 487 | { |
@@ -462,22 +546,55 @@ type AvailableForListIDsOptions = { | |||
462 | } | 546 | } |
463 | ] | 547 | ] |
464 | }, | 548 | }, |
465 | [ ScopeNames.WITH_FILES ]: { | 549 | [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => { |
466 | include: [ | 550 | let subInclude: any[] = [] |
467 | { | 551 | |
468 | model: () => VideoFileModel.unscoped(), | 552 | if (withRedundancies === true) { |
469 | // FIXME: typings | 553 | subInclude = [ |
470 | [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join | 554 | { |
471 | required: false, | 555 | attributes: [ 'fileUrl' ], |
472 | include: [ | 556 | model: VideoRedundancyModel.unscoped(), |
473 | { | 557 | required: false |
474 | attributes: [ 'fileUrl' ], | 558 | } |
475 | model: () => VideoRedundancyModel.unscoped(), | 559 | ] |
476 | required: false | 560 | } |
477 | } | 561 | |
478 | ] | 562 | return { |
479 | } | 563 | include: [ |
480 | ] | 564 | { |
565 | model: VideoFileModel.unscoped(), | ||
566 | // FIXME: typings | ||
567 | [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join | ||
568 | required: false, | ||
569 | include: subInclude | ||
570 | } | ||
571 | ] | ||
572 | } | ||
573 | }, | ||
574 | [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { | ||
575 | let subInclude: any[] = [] | ||
576 | |||
577 | if (withRedundancies === true) { | ||
578 | subInclude = [ | ||
579 | { | ||
580 | attributes: [ 'fileUrl' ], | ||
581 | model: VideoRedundancyModel.unscoped(), | ||
582 | required: false | ||
583 | } | ||
584 | ] | ||
585 | } | ||
586 | |||
587 | return { | ||
588 | include: [ | ||
589 | { | ||
590 | model: VideoStreamingPlaylistModel.unscoped(), | ||
591 | // FIXME: typings | ||
592 | [ 'separate' as any ]: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join | ||
593 | required: false, | ||
594 | include: subInclude | ||
595 | } | ||
596 | ] | ||
597 | } | ||
481 | }, | 598 | }, |
482 | [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { | 599 | [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { |
483 | include: [ | 600 | include: [ |
@@ -661,6 +778,16 @@ export class VideoModel extends Model<VideoModel> { | |||
661 | }) | 778 | }) |
662 | VideoFiles: VideoFileModel[] | 779 | VideoFiles: VideoFileModel[] |
663 | 780 | ||
781 | @HasMany(() => VideoStreamingPlaylistModel, { | ||
782 | foreignKey: { | ||
783 | name: 'videoId', | ||
784 | allowNull: false | ||
785 | }, | ||
786 | hooks: true, | ||
787 | onDelete: 'cascade' | ||
788 | }) | ||
789 | VideoStreamingPlaylists: VideoStreamingPlaylistModel[] | ||
790 | |||
664 | @HasMany(() => VideoShareModel, { | 791 | @HasMany(() => VideoShareModel, { |
665 | foreignKey: { | 792 | foreignKey: { |
666 | name: 'videoId', | 793 | name: 'videoId', |
@@ -725,6 +852,15 @@ export class VideoModel extends Model<VideoModel> { | |||
725 | }) | 852 | }) |
726 | VideoBlacklist: VideoBlacklistModel | 853 | VideoBlacklist: VideoBlacklistModel |
727 | 854 | ||
855 | @HasOne(() => VideoImportModel, { | ||
856 | foreignKey: { | ||
857 | name: 'videoId', | ||
858 | allowNull: true | ||
859 | }, | ||
860 | onDelete: 'set null' | ||
861 | }) | ||
862 | VideoImport: VideoImportModel | ||
863 | |||
728 | @HasMany(() => VideoCaptionModel, { | 864 | @HasMany(() => VideoCaptionModel, { |
729 | foreignKey: { | 865 | foreignKey: { |
730 | name: 'videoId', | 866 | name: 'videoId', |
@@ -777,6 +913,9 @@ export class VideoModel extends Model<VideoModel> { | |||
777 | tasks.push(instance.removeFile(file)) | 913 | tasks.push(instance.removeFile(file)) |
778 | tasks.push(instance.removeTorrent(file)) | 914 | tasks.push(instance.removeTorrent(file)) |
779 | }) | 915 | }) |
916 | |||
917 | // Remove playlists file | ||
918 | tasks.push(instance.removeStreamingPlaylist()) | ||
780 | } | 919 | } |
781 | 920 | ||
782 | // Do not wait video deletion because we could be in a transaction | 921 | // Do not wait video deletion because we could be in a transaction |
@@ -788,8 +927,14 @@ export class VideoModel extends Model<VideoModel> { | |||
788 | return undefined | 927 | return undefined |
789 | } | 928 | } |
790 | 929 | ||
791 | static list () { | 930 | static listLocal () { |
792 | return VideoModel.scope(ScopeNames.WITH_FILES).findAll() | 931 | const query = { |
932 | where: { | ||
933 | remote: false | ||
934 | } | ||
935 | } | ||
936 | |||
937 | return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]).findAll(query) | ||
793 | } | 938 | } |
794 | 939 | ||
795 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { | 940 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { |
@@ -959,10 +1104,15 @@ export class VideoModel extends Model<VideoModel> { | |||
959 | filter?: VideoFilter, | 1104 | filter?: VideoFilter, |
960 | accountId?: number, | 1105 | accountId?: number, |
961 | videoChannelId?: number, | 1106 | videoChannelId?: number, |
962 | actorId?: number | 1107 | followerActorId?: number |
963 | trendingDays?: number, | 1108 | trendingDays?: number, |
964 | userId?: number | 1109 | user?: UserModel, |
1110 | historyOfUser?: UserModel | ||
965 | }, countVideos = true) { | 1111 | }, countVideos = true) { |
1112 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { | ||
1113 | throw new Error('Try to filter all-local but no user has not the see all videos right') | ||
1114 | } | ||
1115 | |||
966 | const query: IFindOptions<VideoModel> = { | 1116 | const query: IFindOptions<VideoModel> = { |
967 | offset: options.start, | 1117 | offset: options.start, |
968 | limit: options.count, | 1118 | limit: options.count, |
@@ -976,11 +1126,14 @@ export class VideoModel extends Model<VideoModel> { | |||
976 | query.group = 'VideoModel.id' | 1126 | query.group = 'VideoModel.id' |
977 | } | 1127 | } |
978 | 1128 | ||
979 | // actorId === null has a meaning, so just check undefined | 1129 | const serverActor = await getServerActor() |
980 | const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id | 1130 | |
1131 | // followerActorId === null has a meaning, so just check undefined | ||
1132 | const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id | ||
981 | 1133 | ||
982 | const queryOptions = { | 1134 | const queryOptions = { |
983 | actorId, | 1135 | followerActorId, |
1136 | serverAccountId: serverActor.Account.id, | ||
984 | nsfw: options.nsfw, | 1137 | nsfw: options.nsfw, |
985 | categoryOneOf: options.categoryOneOf, | 1138 | categoryOneOf: options.categoryOneOf, |
986 | licenceOneOf: options.licenceOneOf, | 1139 | licenceOneOf: options.licenceOneOf, |
@@ -992,7 +1145,8 @@ export class VideoModel extends Model<VideoModel> { | |||
992 | accountId: options.accountId, | 1145 | accountId: options.accountId, |
993 | videoChannelId: options.videoChannelId, | 1146 | videoChannelId: options.videoChannelId, |
994 | includeLocalVideos: options.includeLocalVideos, | 1147 | includeLocalVideos: options.includeLocalVideos, |
995 | userId: options.userId, | 1148 | user: options.user, |
1149 | historyOfUser: options.historyOfUser, | ||
996 | trendingDays | 1150 | trendingDays |
997 | } | 1151 | } |
998 | 1152 | ||
@@ -1015,7 +1169,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1015 | tagsAllOf?: string[] | 1169 | tagsAllOf?: string[] |
1016 | durationMin?: number // seconds | 1170 | durationMin?: number // seconds |
1017 | durationMax?: number // seconds | 1171 | durationMax?: number // seconds |
1018 | userId?: number | 1172 | user?: UserModel, |
1173 | filter?: VideoFilter | ||
1019 | }) { | 1174 | }) { |
1020 | const whereAnd = [] | 1175 | const whereAnd = [] |
1021 | 1176 | ||
@@ -1084,7 +1239,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1084 | 1239 | ||
1085 | const serverActor = await getServerActor() | 1240 | const serverActor = await getServerActor() |
1086 | const queryOptions = { | 1241 | const queryOptions = { |
1087 | actorId: serverActor.id, | 1242 | followerActorId: serverActor.id, |
1243 | serverAccountId: serverActor.Account.id, | ||
1088 | includeLocalVideos: options.includeLocalVideos, | 1244 | includeLocalVideos: options.includeLocalVideos, |
1089 | nsfw: options.nsfw, | 1245 | nsfw: options.nsfw, |
1090 | categoryOneOf: options.categoryOneOf, | 1246 | categoryOneOf: options.categoryOneOf, |
@@ -1092,7 +1248,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1092 | languageOneOf: options.languageOneOf, | 1248 | languageOneOf: options.languageOneOf, |
1093 | tagsOneOf: options.tagsOneOf, | 1249 | tagsOneOf: options.tagsOneOf, |
1094 | tagsAllOf: options.tagsAllOf, | 1250 | tagsAllOf: options.tagsAllOf, |
1095 | userId: options.userId | 1251 | user: options.user, |
1252 | filter: options.filter | ||
1096 | } | 1253 | } |
1097 | 1254 | ||
1098 | return VideoModel.getAvailableForApi(query, queryOptions) | 1255 | return VideoModel.getAvailableForApi(query, queryOptions) |
@@ -1108,6 +1265,16 @@ export class VideoModel extends Model<VideoModel> { | |||
1108 | return VideoModel.findOne(options) | 1265 | return VideoModel.findOne(options) |
1109 | } | 1266 | } |
1110 | 1267 | ||
1268 | static loadWithRights (id: number | string, t?: Sequelize.Transaction) { | ||
1269 | const where = VideoModel.buildWhereIdOrUUID(id) | ||
1270 | const options = { | ||
1271 | where, | ||
1272 | transaction: t | ||
1273 | } | ||
1274 | |||
1275 | return VideoModel.scope([ ScopeNames.WITH_BLACKLISTED, ScopeNames.WITH_USER_ID ]).findOne(options) | ||
1276 | } | ||
1277 | |||
1111 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { | 1278 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { |
1112 | const where = VideoModel.buildWhereIdOrUUID(id) | 1279 | const where = VideoModel.buildWhereIdOrUUID(id) |
1113 | 1280 | ||
@@ -1120,8 +1287,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1120 | return VideoModel.findOne(options) | 1287 | return VideoModel.findOne(options) |
1121 | } | 1288 | } |
1122 | 1289 | ||
1123 | static loadWithFile (id: number, t?: Sequelize.Transaction, logging?: boolean) { | 1290 | static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) { |
1124 | return VideoModel.scope(ScopeNames.WITH_FILES) | 1291 | return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]) |
1125 | .findById(id, { transaction: t, logging }) | 1292 | .findById(id, { transaction: t, logging }) |
1126 | } | 1293 | } |
1127 | 1294 | ||
@@ -1132,9 +1299,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1132 | } | 1299 | } |
1133 | } | 1300 | } |
1134 | 1301 | ||
1135 | return VideoModel | 1302 | return VideoModel.findOne(options) |
1136 | .scope([ ScopeNames.WITH_FILES ]) | ||
1137 | .findOne(options) | ||
1138 | } | 1303 | } |
1139 | 1304 | ||
1140 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { | 1305 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { |
@@ -1156,7 +1321,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1156 | transaction | 1321 | transaction |
1157 | } | 1322 | } |
1158 | 1323 | ||
1159 | return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) | 1324 | return VideoModel.scope([ |
1325 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1326 | ScopeNames.WITH_FILES, | ||
1327 | ScopeNames.WITH_STREAMING_PLAYLISTS | ||
1328 | ]).findOne(query) | ||
1160 | } | 1329 | } |
1161 | 1330 | ||
1162 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { | 1331 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { |
@@ -1171,9 +1340,37 @@ export class VideoModel extends Model<VideoModel> { | |||
1171 | const scopes = [ | 1340 | const scopes = [ |
1172 | ScopeNames.WITH_TAGS, | 1341 | ScopeNames.WITH_TAGS, |
1173 | ScopeNames.WITH_BLACKLISTED, | 1342 | ScopeNames.WITH_BLACKLISTED, |
1343 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1344 | ScopeNames.WITH_SCHEDULED_UPDATE, | ||
1174 | ScopeNames.WITH_FILES, | 1345 | ScopeNames.WITH_FILES, |
1346 | ScopeNames.WITH_STREAMING_PLAYLISTS | ||
1347 | ] | ||
1348 | |||
1349 | if (userId) { | ||
1350 | scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings | ||
1351 | } | ||
1352 | |||
1353 | return VideoModel | ||
1354 | .scope(scopes) | ||
1355 | .findOne(options) | ||
1356 | } | ||
1357 | |||
1358 | static loadForGetAPI (id: number | string, t?: Sequelize.Transaction, userId?: number) { | ||
1359 | const where = VideoModel.buildWhereIdOrUUID(id) | ||
1360 | |||
1361 | const options = { | ||
1362 | order: [ [ 'Tags', 'name', 'ASC' ] ], | ||
1363 | where, | ||
1364 | transaction: t | ||
1365 | } | ||
1366 | |||
1367 | const scopes = [ | ||
1368 | ScopeNames.WITH_TAGS, | ||
1369 | ScopeNames.WITH_BLACKLISTED, | ||
1175 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1370 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1176 | ScopeNames.WITH_SCHEDULED_UPDATE | 1371 | ScopeNames.WITH_SCHEDULED_UPDATE, |
1372 | { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings | ||
1373 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings | ||
1177 | ] | 1374 | ] |
1178 | 1375 | ||
1179 | if (userId) { | 1376 | if (userId) { |
@@ -1217,12 +1414,31 @@ export class VideoModel extends Model<VideoModel> { | |||
1217 | }) | 1414 | }) |
1218 | } | 1415 | } |
1219 | 1416 | ||
1417 | static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) { | ||
1418 | // Instances only share videos | ||
1419 | const query = 'SELECT 1 FROM "videoShare" ' + | ||
1420 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + | ||
1421 | 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' + | ||
1422 | 'LIMIT 1' | ||
1423 | |||
1424 | const options = { | ||
1425 | type: Sequelize.QueryTypes.SELECT, | ||
1426 | bind: { followerActorId, videoId }, | ||
1427 | raw: true | ||
1428 | } | ||
1429 | |||
1430 | return VideoModel.sequelize.query(query, options) | ||
1431 | .then(results => results.length === 1) | ||
1432 | } | ||
1433 | |||
1220 | // threshold corresponds to how many video the field should have to be returned | 1434 | // threshold corresponds to how many video the field should have to be returned |
1221 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { | 1435 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { |
1222 | const actorId = (await getServerActor()).id | 1436 | const serverActor = await getServerActor() |
1437 | const followerActorId = serverActor.id | ||
1223 | 1438 | ||
1224 | const scopeOptions = { | 1439 | const scopeOptions: AvailableForListIDsOptions = { |
1225 | actorId, | 1440 | serverAccountId: serverActor.Account.id, |
1441 | followerActorId, | ||
1226 | includeLocalVideos: true | 1442 | includeLocalVideos: true |
1227 | } | 1443 | } |
1228 | 1444 | ||
@@ -1256,7 +1472,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1256 | } | 1472 | } |
1257 | 1473 | ||
1258 | private static buildActorWhereWithFilter (filter?: VideoFilter) { | 1474 | private static buildActorWhereWithFilter (filter?: VideoFilter) { |
1259 | if (filter && filter === 'local') { | 1475 | if (filter && (filter === 'local' || filter === 'all-local')) { |
1260 | return { | 1476 | return { |
1261 | serverId: null | 1477 | serverId: null |
1262 | } | 1478 | } |
@@ -1267,7 +1483,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1267 | 1483 | ||
1268 | private static async getAvailableForApi ( | 1484 | private static async getAvailableForApi ( |
1269 | query: IFindOptions<VideoModel>, | 1485 | query: IFindOptions<VideoModel>, |
1270 | options: AvailableForListIDsOptions & { userId?: number}, | 1486 | options: AvailableForListIDsOptions, |
1271 | countVideos = true | 1487 | countVideos = true |
1272 | ) { | 1488 | ) { |
1273 | const idsScope = { | 1489 | const idsScope = { |
@@ -1286,7 +1502,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1286 | } | 1502 | } |
1287 | 1503 | ||
1288 | const [ count, rowsId ] = await Promise.all([ | 1504 | const [ count, rowsId ] = await Promise.all([ |
1289 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), | 1505 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined), |
1290 | VideoModel.scope(idsScope).findAll(query) | 1506 | VideoModel.scope(idsScope).findAll(query) |
1291 | ]) | 1507 | ]) |
1292 | const ids = rowsId.map(r => r.id) | 1508 | const ids = rowsId.map(r => r.id) |
@@ -1300,8 +1516,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1300 | } | 1516 | } |
1301 | ] | 1517 | ] |
1302 | 1518 | ||
1303 | if (options.userId) { | 1519 | if (options.user) { |
1304 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] }) | 1520 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) |
1305 | } | 1521 | } |
1306 | 1522 | ||
1307 | const secondQuery = { | 1523 | const secondQuery = { |
@@ -1426,6 +1642,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1426 | videoFile.infoHash = parsedTorrent.infoHash | 1642 | videoFile.infoHash = parsedTorrent.infoHash |
1427 | } | 1643 | } |
1428 | 1644 | ||
1645 | getWatchStaticPath () { | ||
1646 | return '/videos/watch/' + this.uuid | ||
1647 | } | ||
1648 | |||
1429 | getEmbedStaticPath () { | 1649 | getEmbedStaticPath () { |
1430 | return '/videos/embed/' + this.uuid | 1650 | return '/videos/embed/' + this.uuid |
1431 | } | 1651 | } |
@@ -1483,8 +1703,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1483 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) | 1703 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) |
1484 | } | 1704 | } |
1485 | 1705 | ||
1486 | removeFile (videoFile: VideoFileModel) { | 1706 | removeFile (videoFile: VideoFileModel, isRedundancy = false) { |
1487 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | 1707 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR |
1708 | |||
1709 | const filePath = join(baseDir, this.getVideoFilename(videoFile)) | ||
1488 | return remove(filePath) | 1710 | return remove(filePath) |
1489 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | 1711 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) |
1490 | } | 1712 | } |
@@ -1495,6 +1717,14 @@ export class VideoModel extends Model<VideoModel> { | |||
1495 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) | 1717 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) |
1496 | } | 1718 | } |
1497 | 1719 | ||
1720 | removeStreamingPlaylist (isRedundancy = false) { | ||
1721 | const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_PLAYLIST_DIRECTORY | ||
1722 | |||
1723 | const filePath = join(baseDir, this.uuid) | ||
1724 | return remove(filePath) | ||
1725 | .catch(err => logger.warn('Cannot delete playlist directory %s.', filePath, { err })) | ||
1726 | } | ||
1727 | |||
1498 | isOutdated () { | 1728 | isOutdated () { |
1499 | if (this.isOwned()) return false | 1729 | if (this.isOwned()) return false |
1500 | 1730 | ||
@@ -1506,6 +1736,12 @@ export class VideoModel extends Model<VideoModel> { | |||
1506 | (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL | 1736 | (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL |
1507 | } | 1737 | } |
1508 | 1738 | ||
1739 | setAsRefreshed () { | ||
1740 | this.changed('updatedAt', true) | ||
1741 | |||
1742 | return this.save() | ||
1743 | } | ||
1744 | |||
1509 | getBaseUrls () { | 1745 | getBaseUrls () { |
1510 | let baseUrlHttp | 1746 | let baseUrlHttp |
1511 | let baseUrlWs | 1747 | let baseUrlWs |
@@ -1523,7 +1759,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1523 | 1759 | ||
1524 | generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { | 1760 | generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { |
1525 | const xs = this.getTorrentUrl(videoFile, baseUrlHttp) | 1761 | const xs = this.getTorrentUrl(videoFile, baseUrlHttp) |
1526 | const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] | 1762 | const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs) |
1527 | let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] | 1763 | let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] |
1528 | 1764 | ||
1529 | const redundancies = videoFile.RedundancyVideos | 1765 | const redundancies = videoFile.RedundancyVideos |
@@ -1540,6 +1776,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1540 | return magnetUtil.encode(magnetHash) | 1776 | return magnetUtil.encode(magnetHash) |
1541 | } | 1777 | } |
1542 | 1778 | ||
1779 | getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) { | ||
1780 | return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] | ||
1781 | } | ||
1782 | |||
1543 | getThumbnailUrl (baseUrlHttp: string) { | 1783 | getThumbnailUrl (baseUrlHttp: string) { |
1544 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() | 1784 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() |
1545 | } | 1785 | } |
@@ -1556,7 +1796,15 @@ export class VideoModel extends Model<VideoModel> { | |||
1556 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) | 1796 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) |
1557 | } | 1797 | } |
1558 | 1798 | ||
1799 | getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | ||
1800 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile) | ||
1801 | } | ||
1802 | |||
1559 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | 1803 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1560 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) | 1804 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) |
1561 | } | 1805 | } |
1806 | |||
1807 | getBandwidthBits (videoFile: VideoFileModel) { | ||
1808 | return Math.ceil((videoFile.size * 8) / this.duration) | ||
1809 | } | ||
1562 | } | 1810 | } |
diff --git a/server/tests/activitypub.ts b/server/tests/activitypub.ts deleted file mode 100644 index 53a04d363..000000000 --- a/server/tests/activitypub.ts +++ /dev/null | |||
@@ -1,35 +0,0 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { flushTests, killallServers, makeActivityPubGetRequest, runServer, ServerInfo, setAccessTokensToServers } from './utils' | ||
6 | |||
7 | const expect = chai.expect | ||
8 | |||
9 | describe('Test activitypub', function () { | ||
10 | let server: ServerInfo = null | ||
11 | |||
12 | before(async function () { | ||
13 | this.timeout(30000) | ||
14 | |||
15 | await flushTests() | ||
16 | |||
17 | server = await runServer(1) | ||
18 | |||
19 | await setAccessTokensToServers([ server ]) | ||
20 | }) | ||
21 | |||
22 | it('Should return the account object', async function () { | ||
23 | const res = await makeActivityPubGetRequest(server.url, '/accounts/root') | ||
24 | const object = res.body | ||
25 | |||
26 | expect(object.type).to.equal('Person') | ||
27 | expect(object.id).to.equal('http://localhost:9001/accounts/root') | ||
28 | expect(object.name).to.equal('root') | ||
29 | expect(object.preferredUsername).to.equal('root') | ||
30 | }) | ||
31 | |||
32 | after(async function () { | ||
33 | killallServers([ server ]) | ||
34 | }) | ||
35 | }) | ||
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts new file mode 100644 index 000000000..6d90d8643 --- /dev/null +++ b/server/tests/api/activitypub/client.ts | |||
@@ -0,0 +1,67 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | doubleFollow, | ||
7 | flushAndRunMultipleServers, | ||
8 | flushTests, | ||
9 | killallServers, | ||
10 | makeActivityPubGetRequest, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers, | ||
13 | uploadVideo | ||
14 | } from '../../../../shared/utils' | ||
15 | |||
16 | const expect = chai.expect | ||
17 | |||
18 | describe('Test activitypub', function () { | ||
19 | let servers: ServerInfo[] = [] | ||
20 | let videoUUID: string | ||
21 | |||
22 | before(async function () { | ||
23 | this.timeout(30000) | ||
24 | |||
25 | await flushTests() | ||
26 | |||
27 | servers = await flushAndRunMultipleServers(2) | ||
28 | |||
29 | await setAccessTokensToServers(servers) | ||
30 | |||
31 | { | ||
32 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' }) | ||
33 | videoUUID = res.body.video.uuid | ||
34 | } | ||
35 | |||
36 | await doubleFollow(servers[0], servers[1]) | ||
37 | }) | ||
38 | |||
39 | it('Should return the account object', async function () { | ||
40 | const res = await makeActivityPubGetRequest(servers[0].url, '/accounts/root') | ||
41 | const object = res.body | ||
42 | |||
43 | expect(object.type).to.equal('Person') | ||
44 | expect(object.id).to.equal('http://localhost:9001/accounts/root') | ||
45 | expect(object.name).to.equal('root') | ||
46 | expect(object.preferredUsername).to.equal('root') | ||
47 | }) | ||
48 | |||
49 | it('Should return the video object', async function () { | ||
50 | const res = await makeActivityPubGetRequest(servers[0].url, '/videos/watch/' + videoUUID) | ||
51 | const object = res.body | ||
52 | |||
53 | expect(object.type).to.equal('Video') | ||
54 | expect(object.id).to.equal('http://localhost:9001/videos/watch/' + videoUUID) | ||
55 | expect(object.name).to.equal('video') | ||
56 | }) | ||
57 | |||
58 | it('Should redirect to the origin video object', async function () { | ||
59 | const res = await makeActivityPubGetRequest(servers[1].url, '/videos/watch/' + videoUUID, 302) | ||
60 | |||
61 | expect(res.header.location).to.equal('http://localhost:9001/videos/watch/' + videoUUID) | ||
62 | }) | ||
63 | |||
64 | after(async function () { | ||
65 | killallServers(servers) | ||
66 | }) | ||
67 | }) | ||
diff --git a/server/tests/api/activitypub/fetch.ts b/server/tests/api/activitypub/fetch.ts new file mode 100644 index 000000000..03609c1a9 --- /dev/null +++ b/server/tests/api/activitypub/fetch.ts | |||
@@ -0,0 +1,87 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | |||
5 | import { | ||
6 | createUser, | ||
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
9 | flushTests, | ||
10 | getVideosListSort, | ||
11 | killallServers, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | setActorField, | ||
15 | setVideoField, | ||
16 | uploadVideo, | ||
17 | userLogin, | ||
18 | waitJobs | ||
19 | } from '../../../../shared/utils' | ||
20 | import * as chai from 'chai' | ||
21 | import { Video } from '../../../../shared/models/videos' | ||
22 | |||
23 | const expect = chai.expect | ||
24 | |||
25 | describe('Test ActivityPub fetcher', function () { | ||
26 | let servers: ServerInfo[] | ||
27 | |||
28 | // --------------------------------------------------------------- | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(60000) | ||
32 | |||
33 | servers = await flushAndRunMultipleServers(3) | ||
34 | |||
35 | // Get the access tokens | ||
36 | await setAccessTokensToServers(servers) | ||
37 | |||
38 | const user = { username: 'user1', password: 'password' } | ||
39 | for (const server of servers) { | ||
40 | await createUser(server.url, server.accessToken, user.username, user.password) | ||
41 | } | ||
42 | |||
43 | const userAccessToken = await userLogin(servers[0], user) | ||
44 | |||
45 | await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video root' }) | ||
46 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'bad video root' }) | ||
47 | const badVideoUUID = res.body.video.uuid | ||
48 | await uploadVideo(servers[0].url, userAccessToken, { name: 'video user' }) | ||
49 | |||
50 | await setActorField(1, 'http://localhost:9001/accounts/user1', 'url', 'http://localhost:9002/accounts/user1') | ||
51 | await setVideoField(1, badVideoUUID, 'url', 'http://localhost:9003/videos/watch/' + badVideoUUID) | ||
52 | }) | ||
53 | |||
54 | it('Should add only the video with a valid actor URL', async function () { | ||
55 | this.timeout(60000) | ||
56 | |||
57 | await doubleFollow(servers[0], servers[1]) | ||
58 | await waitJobs(servers) | ||
59 | |||
60 | { | ||
61 | const res = await getVideosListSort(servers[0].url, 'createdAt') | ||
62 | expect(res.body.total).to.equal(3) | ||
63 | |||
64 | const data: Video[] = res.body.data | ||
65 | expect(data[0].name).to.equal('video root') | ||
66 | expect(data[1].name).to.equal('bad video root') | ||
67 | expect(data[2].name).to.equal('video user') | ||
68 | } | ||
69 | |||
70 | { | ||
71 | const res = await getVideosListSort(servers[1].url, 'createdAt') | ||
72 | expect(res.body.total).to.equal(1) | ||
73 | |||
74 | const data: Video[] = res.body.data | ||
75 | expect(data[0].name).to.equal('video root') | ||
76 | } | ||
77 | }) | ||
78 | |||
79 | after(async function () { | ||
80 | killallServers(servers) | ||
81 | |||
82 | // Keep the logs if the test failed | ||
83 | if (this['ok']) { | ||
84 | await flushTests() | ||
85 | } | ||
86 | }) | ||
87 | }) | ||
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts new file mode 100644 index 000000000..ac6e755c3 --- /dev/null +++ b/server/tests/api/activitypub/helpers.ts | |||
@@ -0,0 +1,182 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { expect } from 'chai' | ||
5 | import { buildRequestStub } from '../../../../shared/utils/miscs/stubs' | ||
6 | import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' | ||
7 | import { cloneDeep } from 'lodash' | ||
8 | import { buildSignedActivity } from '../../../helpers/activitypub' | ||
9 | |||
10 | describe('Test activity pub helpers', function () { | ||
11 | describe('When checking the Linked Signature', function () { | ||
12 | |||
13 | it('Should fail with an invalid Mastodon signature', async function () { | ||
14 | const body = require('./json/mastodon/create-bad-signature.json') | ||
15 | const publicKey = require('./json/mastodon/public-key.json').publicKey | ||
16 | const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } | ||
17 | |||
18 | const result = await isJsonLDSignatureVerified(fromActor as any, body) | ||
19 | |||
20 | expect(result).to.be.false | ||
21 | }) | ||
22 | |||
23 | it('Should fail with an invalid public key', async function () { | ||
24 | const body = require('./json/mastodon/create.json') | ||
25 | const publicKey = require('./json/mastodon/bad-public-key.json').publicKey | ||
26 | const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } | ||
27 | |||
28 | const result = await isJsonLDSignatureVerified(fromActor as any, body) | ||
29 | |||
30 | expect(result).to.be.false | ||
31 | }) | ||
32 | |||
33 | it('Should succeed with a valid Mastodon signature', async function () { | ||
34 | const body = require('./json/mastodon/create.json') | ||
35 | const publicKey = require('./json/mastodon/public-key.json').publicKey | ||
36 | const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } | ||
37 | |||
38 | const result = await isJsonLDSignatureVerified(fromActor as any, body) | ||
39 | |||
40 | expect(result).to.be.true | ||
41 | }) | ||
42 | |||
43 | it('Should fail with an invalid PeerTube signature', async function () { | ||
44 | const keys = require('./json/peertube/invalid-keys.json') | ||
45 | const body = require('./json/peertube/announce-without-context.json') | ||
46 | |||
47 | const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } | ||
48 | const signedBody = await buildSignedActivity(actorSignature as any, body) | ||
49 | |||
50 | const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' } | ||
51 | const result = await isJsonLDSignatureVerified(fromActor as any, signedBody) | ||
52 | |||
53 | expect(result).to.be.false | ||
54 | }) | ||
55 | |||
56 | it('Should fail with an invalid PeerTube URL', async function () { | ||
57 | const keys = require('./json/peertube/keys.json') | ||
58 | const body = require('./json/peertube/announce-without-context.json') | ||
59 | |||
60 | const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } | ||
61 | const signedBody = await buildSignedActivity(actorSignature as any, body) | ||
62 | |||
63 | const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9003/accounts/peertube' } | ||
64 | const result = await isJsonLDSignatureVerified(fromActor as any, signedBody) | ||
65 | |||
66 | expect(result).to.be.false | ||
67 | }) | ||
68 | |||
69 | it('Should succeed with a valid PeerTube signature', async function () { | ||
70 | const keys = require('./json/peertube/keys.json') | ||
71 | const body = require('./json/peertube/announce-without-context.json') | ||
72 | |||
73 | const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } | ||
74 | const signedBody = await buildSignedActivity(actorSignature as any, body) | ||
75 | |||
76 | const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' } | ||
77 | const result = await isJsonLDSignatureVerified(fromActor as any, signedBody) | ||
78 | |||
79 | expect(result).to.be.true | ||
80 | }) | ||
81 | }) | ||
82 | |||
83 | describe('When checking HTTP signature', function () { | ||
84 | it('Should fail with an invalid http signature', async function () { | ||
85 | const req = buildRequestStub() | ||
86 | req.method = 'POST' | ||
87 | req.url = '/accounts/ronan/inbox' | ||
88 | |||
89 | const mastodonObject = cloneDeep(require('./json/mastodon/bad-http-signature.json')) | ||
90 | req.body = mastodonObject.body | ||
91 | req.headers = mastodonObject.headers | ||
92 | req.headers.signature = 'Signature ' + req.headers.signature | ||
93 | |||
94 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | ||
95 | const publicKey = require('./json/mastodon/public-key.json').publicKey | ||
96 | |||
97 | const actor = { publicKey } | ||
98 | const verified = isHTTPSignatureVerified(parsed, actor as any) | ||
99 | |||
100 | expect(verified).to.be.false | ||
101 | }) | ||
102 | |||
103 | it('Should fail with an invalid public key', async function () { | ||
104 | const req = buildRequestStub() | ||
105 | req.method = 'POST' | ||
106 | req.url = '/accounts/ronan/inbox' | ||
107 | |||
108 | const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) | ||
109 | req.body = mastodonObject.body | ||
110 | req.headers = mastodonObject.headers | ||
111 | req.headers.signature = 'Signature ' + req.headers.signature | ||
112 | |||
113 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | ||
114 | const publicKey = require('./json/mastodon/bad-public-key.json').publicKey | ||
115 | |||
116 | const actor = { publicKey } | ||
117 | const verified = isHTTPSignatureVerified(parsed, actor as any) | ||
118 | |||
119 | expect(verified).to.be.false | ||
120 | }) | ||
121 | |||
122 | it('Should fail because of clock skew', async function () { | ||
123 | const req = buildRequestStub() | ||
124 | req.method = 'POST' | ||
125 | req.url = '/accounts/ronan/inbox' | ||
126 | |||
127 | const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) | ||
128 | req.body = mastodonObject.body | ||
129 | req.headers = mastodonObject.headers | ||
130 | req.headers.signature = 'Signature ' + req.headers.signature | ||
131 | |||
132 | let errored = false | ||
133 | try { | ||
134 | parseHTTPSignature(req) | ||
135 | } catch { | ||
136 | errored = true | ||
137 | } | ||
138 | |||
139 | expect(errored).to.be.true | ||
140 | }) | ||
141 | |||
142 | it('Should fail without scheme', async function () { | ||
143 | const req = buildRequestStub() | ||
144 | req.method = 'POST' | ||
145 | req.url = '/accounts/ronan/inbox' | ||
146 | |||
147 | const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) | ||
148 | req.body = mastodonObject.body | ||
149 | req.headers = mastodonObject.headers | ||
150 | |||
151 | let errored = false | ||
152 | try { | ||
153 | parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | ||
154 | } catch { | ||
155 | errored = true | ||
156 | } | ||
157 | |||
158 | expect(errored).to.be.true | ||
159 | }) | ||
160 | |||
161 | it('Should succeed with a valid signature', async function () { | ||
162 | const req = buildRequestStub() | ||
163 | req.method = 'POST' | ||
164 | req.url = '/accounts/ronan/inbox' | ||
165 | |||
166 | const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) | ||
167 | req.body = mastodonObject.body | ||
168 | req.headers = mastodonObject.headers | ||
169 | req.headers.signature = 'Signature ' + req.headers.signature | ||
170 | |||
171 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | ||
172 | const publicKey = require('./json/mastodon/public-key.json').publicKey | ||
173 | |||
174 | const actor = { publicKey } | ||
175 | const verified = isHTTPSignatureVerified(parsed, actor as any) | ||
176 | |||
177 | expect(verified).to.be.true | ||
178 | }) | ||
179 | |||
180 | }) | ||
181 | |||
182 | }) | ||
diff --git a/server/tests/api/activitypub/index.ts b/server/tests/api/activitypub/index.ts new file mode 100644 index 000000000..450053309 --- /dev/null +++ b/server/tests/api/activitypub/index.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | import './client' | ||
2 | import './fetch' | ||
3 | import './helpers' | ||
4 | import './refresher' | ||
5 | import './security' | ||
diff --git a/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json b/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json new file mode 100644 index 000000000..4e7bc3af5 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json | |||
@@ -0,0 +1,93 @@ | |||
1 | { | ||
2 | "headers": { | ||
3 | "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)", | ||
4 | "host": "localhost", | ||
5 | "date": "Mon, 22 Oct 2018 13:34:22 GMT", | ||
6 | "accept-encoding": "gzip", | ||
7 | "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=", | ||
8 | "content-type": "application/activity+json", | ||
9 | "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"", | ||
10 | "content-length": "2815" | ||
11 | }, | ||
12 | "body": { | ||
13 | "@context": [ | ||
14 | "https://www.w3.org/ns/activitystreams", | ||
15 | "https://w3id.org/security/v1", | ||
16 | { | ||
17 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", | ||
18 | "sensitive": "as:sensitive", | ||
19 | "movedTo": { | ||
20 | "@id": "as:movedTo", | ||
21 | "@type": "@id" | ||
22 | }, | ||
23 | "Hashtag": "as:Hashtag", | ||
24 | "ostatus": "http://ostatus.org#", | ||
25 | "atomUri": "ostatus:atomUri", | ||
26 | "inReplyToAtomUri": "ostatus:inReplyToAtomUri", | ||
27 | "conversation": "ostatus:conversation", | ||
28 | "toot": "http://joinmastodon.org/ns#", | ||
29 | "Emoji": "toot:Emoji", | ||
30 | "focalPoint": { | ||
31 | "@container": "@list", | ||
32 | "@id": "toot:focalPoint" | ||
33 | }, | ||
34 | "featured": { | ||
35 | "@id": "toot:featured", | ||
36 | "@type": "@id" | ||
37 | }, | ||
38 | "schema": "http://schema.org#", | ||
39 | "PropertyValue": "schema:PropertyValue", | ||
40 | "value": "schema:value" | ||
41 | } | ||
42 | ], | ||
43 | "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity", | ||
44 | "type": "Create", | ||
45 | "actor": "http://localhost:3000/users/ronan2", | ||
46 | "published": "2018-10-22T13:34:18Z", | ||
47 | "to": [ | ||
48 | "https://www.w3.org/ns/activitystreams#Public" | ||
49 | ], | ||
50 | "cc": [ | ||
51 | "http://localhost:3000/users/ronan2/followers", | ||
52 | "http://localhost:9000/accounts/ronan" | ||
53 | ], | ||
54 | "object": { | ||
55 | "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948", | ||
56 | "type": "Note", | ||
57 | "summary": null, | ||
58 | "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", | ||
59 | "published": "2018-10-22T13:34:18Z", | ||
60 | "url": "http://localhost:3000/@ronan2/100939547203370948", | ||
61 | "attributedTo": "http://localhost:3000/users/ronan2", | ||
62 | "to": [ | ||
63 | "https://www.w3.org/ns/activitystreams#Public" | ||
64 | ], | ||
65 | "cc": [ | ||
66 | "http://localhost:3000/users/ronan2/followers", | ||
67 | "http://localhost:9000/accounts/ronan" | ||
68 | ], | ||
69 | "sensitive": false, | ||
70 | "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948", | ||
71 | "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", | ||
72 | "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation", | ||
73 | "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>", | ||
74 | "contentMap": { | ||
75 | "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>" | ||
76 | }, | ||
77 | "attachment": [], | ||
78 | "tag": [ | ||
79 | { | ||
80 | "type": "Mention", | ||
81 | "href": "http://localhost:9000/accounts/ronan", | ||
82 | "name": "@ronan@localhost:9000" | ||
83 | } | ||
84 | ] | ||
85 | }, | ||
86 | "signature": { | ||
87 | "type": "RsaSignature2017", | ||
88 | "creator": "http://localhost:3000/users/ronan2#main-key", | ||
89 | "created": "2018-10-22T13:34:19Z", | ||
90 | "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q==" | ||
91 | } | ||
92 | } | ||
93 | } | ||
diff --git a/server/tests/api/activitypub/json/mastodon/bad-http-signature.json b/server/tests/api/activitypub/json/mastodon/bad-http-signature.json new file mode 100644 index 000000000..098597db0 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/bad-http-signature.json | |||
@@ -0,0 +1,93 @@ | |||
1 | { | ||
2 | "headers": { | ||
3 | "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)", | ||
4 | "host": "localhost", | ||
5 | "date": "Mon, 22 Oct 2018 13:34:22 GMT", | ||
6 | "accept-encoding": "gzip", | ||
7 | "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=", | ||
8 | "content-type": "application/activity+json", | ||
9 | "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl4wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"", | ||
10 | "content-length": "2815" | ||
11 | }, | ||
12 | "body": { | ||
13 | "@context": [ | ||
14 | "https://www.w3.org/ns/activitystreams", | ||
15 | "https://w3id.org/security/v1", | ||
16 | { | ||
17 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", | ||
18 | "sensitive": "as:sensitive", | ||
19 | "movedTo": { | ||
20 | "@id": "as:movedTo", | ||
21 | "@type": "@id" | ||
22 | }, | ||
23 | "Hashtag": "as:Hashtag", | ||
24 | "ostatus": "http://ostatus.org#", | ||
25 | "atomUri": "ostatus:atomUri", | ||
26 | "inReplyToAtomUri": "ostatus:inReplyToAtomUri", | ||
27 | "conversation": "ostatus:conversation", | ||
28 | "toot": "http://joinmastodon.org/ns#", | ||
29 | "Emoji": "toot:Emoji", | ||
30 | "focalPoint": { | ||
31 | "@container": "@list", | ||
32 | "@id": "toot:focalPoint" | ||
33 | }, | ||
34 | "featured": { | ||
35 | "@id": "toot:featured", | ||
36 | "@type": "@id" | ||
37 | }, | ||
38 | "schema": "http://schema.org#", | ||
39 | "PropertyValue": "schema:PropertyValue", | ||
40 | "value": "schema:value" | ||
41 | } | ||
42 | ], | ||
43 | "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity", | ||
44 | "type": "Create", | ||
45 | "actor": "http://localhost:3000/users/ronan2", | ||
46 | "published": "2018-10-22T13:34:18Z", | ||
47 | "to": [ | ||
48 | "https://www.w3.org/ns/activitystreams#Public" | ||
49 | ], | ||
50 | "cc": [ | ||
51 | "http://localhost:3000/users/ronan2/followers", | ||
52 | "http://localhost:9000/accounts/ronan" | ||
53 | ], | ||
54 | "object": { | ||
55 | "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948", | ||
56 | "type": "Note", | ||
57 | "summary": null, | ||
58 | "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", | ||
59 | "published": "2018-10-22T13:34:18Z", | ||
60 | "url": "http://localhost:3000/@ronan2/100939547203370948", | ||
61 | "attributedTo": "http://localhost:3000/users/ronan2", | ||
62 | "to": [ | ||
63 | "https://www.w3.org/ns/activitystreams#Public" | ||
64 | ], | ||
65 | "cc": [ | ||
66 | "http://localhost:3000/users/ronan2/followers", | ||
67 | "http://localhost:9000/accounts/ronan" | ||
68 | ], | ||
69 | "sensitive": false, | ||
70 | "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948", | ||
71 | "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", | ||
72 | "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation", | ||
73 | "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>", | ||
74 | "contentMap": { | ||
75 | "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>" | ||
76 | }, | ||
77 | "attachment": [], | ||
78 | "tag": [ | ||
79 | { | ||
80 | "type": "Mention", | ||
81 | "href": "http://localhost:9000/accounts/ronan", | ||
82 | "name": "@ronan@localhost:9000" | ||
83 | } | ||
84 | ] | ||
85 | }, | ||
86 | "signature": { | ||
87 | "type": "RsaSignature2017", | ||
88 | "creator": "http://localhost:3000/users/ronan2#main-key", | ||
89 | "created": "2018-10-22T13:34:19Z", | ||
90 | "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q==" | ||
91 | } | ||
92 | } | ||
93 | } | ||
diff --git a/server/tests/api/activitypub/json/mastodon/bad-public-key.json b/server/tests/api/activitypub/json/mastodon/bad-public-key.json new file mode 100644 index 000000000..73d18b3ad --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/bad-public-key.json | |||
@@ -0,0 +1,3 @@ | |||
1 | { | ||
2 | "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl77j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n" | ||
3 | } | ||
diff --git a/server/tests/api/activitypub/json/mastodon/create-bad-signature.json b/server/tests/api/activitypub/json/mastodon/create-bad-signature.json new file mode 100644 index 000000000..2cd037241 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/create-bad-signature.json | |||
@@ -0,0 +1,81 @@ | |||
1 | { | ||
2 | "@context": [ | ||
3 | "https://www.w3.org/ns/activitystreams", | ||
4 | "https://w3id.org/security/v1", | ||
5 | { | ||
6 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", | ||
7 | "sensitive": "as:sensitive", | ||
8 | "movedTo": { | ||
9 | "@id": "as:movedTo", | ||
10 | "@type": "@id" | ||
11 | }, | ||
12 | "Hashtag": "as:Hashtag", | ||
13 | "ostatus": "http://ostatus.org#", | ||
14 | "atomUri": "ostatus:atomUri", | ||
15 | "inReplyToAtomUri": "ostatus:inReplyToAtomUri", | ||
16 | "conversation": "ostatus:conversation", | ||
17 | "toot": "http://joinmastodon.org/ns#", | ||
18 | "Emoji": "toot:Emoji", | ||
19 | "focalPoint": { | ||
20 | "@container": "@list", | ||
21 | "@id": "toot:focalPoint" | ||
22 | }, | ||
23 | "featured": { | ||
24 | "@id": "toot:featured", | ||
25 | "@type": "@id" | ||
26 | }, | ||
27 | "schema": "http://schema.org#", | ||
28 | "PropertyValue": "schema:PropertyValue", | ||
29 | "value": "schema:value" | ||
30 | } | ||
31 | ], | ||
32 | "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity", | ||
33 | "type": "Create", | ||
34 | "actor": "http://localhost:3000/users/ronan2", | ||
35 | "published": "2018-10-22T12:43:07Z", | ||
36 | "to": [ | ||
37 | "https://www.w3.org/ns/activitystreams#Public" | ||
38 | ], | ||
39 | "cc": [ | ||
40 | "http://localhost:3000/users/ronan2/followers", | ||
41 | "http://localhost:9000/accounts/ronan" | ||
42 | ], | ||
43 | "object": { | ||
44 | "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698", | ||
45 | "type": "Note", | ||
46 | "summary": null, | ||
47 | "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", | ||
48 | "published": "2018-10-22T12:43:07Z", | ||
49 | "url": "http://localhost:3000/@ronan2/100939345950887698", | ||
50 | "attributedTo": "http://localhost:3000/users/ronan2", | ||
51 | "to": [ | ||
52 | "https://www.w3.org/ns/activitystreams#Public" | ||
53 | ], | ||
54 | "cc": [ | ||
55 | "http://localhost:3000/users/ronan2/followers", | ||
56 | "http://localhost:9000/accounts/ronan" | ||
57 | ], | ||
58 | "sensitive": false, | ||
59 | "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698", | ||
60 | "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", | ||
61 | "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation", | ||
62 | "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>", | ||
63 | "contentMap": { | ||
64 | "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>" | ||
65 | }, | ||
66 | "attachment": [], | ||
67 | "tag": [ | ||
68 | { | ||
69 | "type": "Mention", | ||
70 | "href": "http://localhost:9000/accounts/ronan", | ||
71 | "name": "@ronan@localhost:9000" | ||
72 | } | ||
73 | ] | ||
74 | }, | ||
75 | "signature": { | ||
76 | "type": "RsaSignature2017", | ||
77 | "creator": "http://localhost:3000/users/ronan2#main-key", | ||
78 | "created": "2018-10-22T12:43:08Z", | ||
79 | "signatureValue": "Vgr8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ==" | ||
80 | } | ||
81 | } | ||
diff --git a/server/tests/api/activitypub/json/mastodon/create.json b/server/tests/api/activitypub/json/mastodon/create.json new file mode 100644 index 000000000..0be271bb8 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/create.json | |||
@@ -0,0 +1,81 @@ | |||
1 | { | ||
2 | "@context": [ | ||
3 | "https://www.w3.org/ns/activitystreams", | ||
4 | "https://w3id.org/security/v1", | ||
5 | { | ||
6 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", | ||
7 | "sensitive": "as:sensitive", | ||
8 | "movedTo": { | ||
9 | "@id": "as:movedTo", | ||
10 | "@type": "@id" | ||
11 | }, | ||
12 | "Hashtag": "as:Hashtag", | ||
13 | "ostatus": "http://ostatus.org#", | ||
14 | "atomUri": "ostatus:atomUri", | ||
15 | "inReplyToAtomUri": "ostatus:inReplyToAtomUri", | ||
16 | "conversation": "ostatus:conversation", | ||
17 | "toot": "http://joinmastodon.org/ns#", | ||
18 | "Emoji": "toot:Emoji", | ||
19 | "focalPoint": { | ||
20 | "@container": "@list", | ||
21 | "@id": "toot:focalPoint" | ||
22 | }, | ||
23 | "featured": { | ||
24 | "@id": "toot:featured", | ||
25 | "@type": "@id" | ||
26 | }, | ||
27 | "schema": "http://schema.org#", | ||
28 | "PropertyValue": "schema:PropertyValue", | ||
29 | "value": "schema:value" | ||
30 | } | ||
31 | ], | ||
32 | "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity", | ||
33 | "type": "Create", | ||
34 | "actor": "http://localhost:3000/users/ronan2", | ||
35 | "published": "2018-10-22T12:43:07Z", | ||
36 | "to": [ | ||
37 | "https://www.w3.org/ns/activitystreams#Public" | ||
38 | ], | ||
39 | "cc": [ | ||
40 | "http://localhost:3000/users/ronan2/followers", | ||
41 | "http://localhost:9000/accounts/ronan" | ||
42 | ], | ||
43 | "object": { | ||
44 | "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698", | ||
45 | "type": "Note", | ||
46 | "summary": null, | ||
47 | "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", | ||
48 | "published": "2018-10-22T12:43:07Z", | ||
49 | "url": "http://localhost:3000/@ronan2/100939345950887698", | ||
50 | "attributedTo": "http://localhost:3000/users/ronan2", | ||
51 | "to": [ | ||
52 | "https://www.w3.org/ns/activitystreams#Public" | ||
53 | ], | ||
54 | "cc": [ | ||
55 | "http://localhost:3000/users/ronan2/followers", | ||
56 | "http://localhost:9000/accounts/ronan" | ||
57 | ], | ||
58 | "sensitive": false, | ||
59 | "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698", | ||
60 | "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", | ||
61 | "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation", | ||
62 | "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>", | ||
63 | "contentMap": { | ||
64 | "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>" | ||
65 | }, | ||
66 | "attachment": [], | ||
67 | "tag": [ | ||
68 | { | ||
69 | "type": "Mention", | ||
70 | "href": "http://localhost:9000/accounts/ronan", | ||
71 | "name": "@ronan@localhost:9000" | ||
72 | } | ||
73 | ] | ||
74 | }, | ||
75 | "signature": { | ||
76 | "type": "RsaSignature2017", | ||
77 | "creator": "http://localhost:3000/users/ronan2#main-key", | ||
78 | "created": "2018-10-22T12:43:08Z", | ||
79 | "signatureValue": "VgR8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ==" | ||
80 | } | ||
81 | } | ||
diff --git a/server/tests/api/activitypub/json/mastodon/http-signature.json b/server/tests/api/activitypub/json/mastodon/http-signature.json new file mode 100644 index 000000000..4e7bc3af5 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/http-signature.json | |||
@@ -0,0 +1,93 @@ | |||
1 | { | ||
2 | "headers": { | ||
3 | "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)", | ||
4 | "host": "localhost", | ||
5 | "date": "Mon, 22 Oct 2018 13:34:22 GMT", | ||
6 | "accept-encoding": "gzip", | ||
7 | "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=", | ||
8 | "content-type": "application/activity+json", | ||
9 | "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"", | ||
10 | "content-length": "2815" | ||
11 | }, | ||
12 | "body": { | ||
13 | "@context": [ | ||
14 | "https://www.w3.org/ns/activitystreams", | ||
15 | "https://w3id.org/security/v1", | ||
16 | { | ||
17 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", | ||
18 | "sensitive": "as:sensitive", | ||
19 | "movedTo": { | ||
20 | "@id": "as:movedTo", | ||
21 | "@type": "@id" | ||
22 | }, | ||
23 | "Hashtag": "as:Hashtag", | ||
24 | "ostatus": "http://ostatus.org#", | ||
25 | "atomUri": "ostatus:atomUri", | ||
26 | "inReplyToAtomUri": "ostatus:inReplyToAtomUri", | ||
27 | "conversation": "ostatus:conversation", | ||
28 | "toot": "http://joinmastodon.org/ns#", | ||
29 | "Emoji": "toot:Emoji", | ||
30 | "focalPoint": { | ||
31 | "@container": "@list", | ||
32 | "@id": "toot:focalPoint" | ||
33 | }, | ||
34 | "featured": { | ||
35 | "@id": "toot:featured", | ||
36 | "@type": "@id" | ||
37 | }, | ||
38 | "schema": "http://schema.org#", | ||
39 | "PropertyValue": "schema:PropertyValue", | ||
40 | "value": "schema:value" | ||
41 | } | ||
42 | ], | ||
43 | "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity", | ||
44 | "type": "Create", | ||
45 | "actor": "http://localhost:3000/users/ronan2", | ||
46 | "published": "2018-10-22T13:34:18Z", | ||
47 | "to": [ | ||
48 | "https://www.w3.org/ns/activitystreams#Public" | ||
49 | ], | ||
50 | "cc": [ | ||
51 | "http://localhost:3000/users/ronan2/followers", | ||
52 | "http://localhost:9000/accounts/ronan" | ||
53 | ], | ||
54 | "object": { | ||
55 | "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948", | ||
56 | "type": "Note", | ||
57 | "summary": null, | ||
58 | "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", | ||
59 | "published": "2018-10-22T13:34:18Z", | ||
60 | "url": "http://localhost:3000/@ronan2/100939547203370948", | ||
61 | "attributedTo": "http://localhost:3000/users/ronan2", | ||
62 | "to": [ | ||
63 | "https://www.w3.org/ns/activitystreams#Public" | ||
64 | ], | ||
65 | "cc": [ | ||
66 | "http://localhost:3000/users/ronan2/followers", | ||
67 | "http://localhost:9000/accounts/ronan" | ||
68 | ], | ||
69 | "sensitive": false, | ||
70 | "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948", | ||
71 | "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", | ||
72 | "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation", | ||
73 | "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>", | ||
74 | "contentMap": { | ||
75 | "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>" | ||
76 | }, | ||
77 | "attachment": [], | ||
78 | "tag": [ | ||
79 | { | ||
80 | "type": "Mention", | ||
81 | "href": "http://localhost:9000/accounts/ronan", | ||
82 | "name": "@ronan@localhost:9000" | ||
83 | } | ||
84 | ] | ||
85 | }, | ||
86 | "signature": { | ||
87 | "type": "RsaSignature2017", | ||
88 | "creator": "http://localhost:3000/users/ronan2#main-key", | ||
89 | "created": "2018-10-22T13:34:19Z", | ||
90 | "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q==" | ||
91 | } | ||
92 | } | ||
93 | } | ||
diff --git a/server/tests/api/activitypub/json/mastodon/public-key.json b/server/tests/api/activitypub/json/mastodon/public-key.json new file mode 100644 index 000000000..b7b9b8308 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/public-key.json | |||
@@ -0,0 +1,3 @@ | |||
1 | { | ||
2 | "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl87j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n" | ||
3 | } | ||
diff --git a/server/tests/api/activitypub/json/peertube/announce-without-context.json b/server/tests/api/activitypub/json/peertube/announce-without-context.json new file mode 100644 index 000000000..5f2af0cde --- /dev/null +++ b/server/tests/api/activitypub/json/peertube/announce-without-context.json | |||
@@ -0,0 +1,13 @@ | |||
1 | { | ||
2 | "type": "Announce", | ||
3 | "id": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05/announces/1", | ||
4 | "actor": "http://localhost:9002/accounts/peertube", | ||
5 | "object": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05", | ||
6 | "to": [ | ||
7 | "https://www.w3.org/ns/activitystreams#Public", | ||
8 | "http://localhost:9002/accounts/peertube/followers", | ||
9 | "http://localhost:9002/video-channels/root_channel/followers", | ||
10 | "http://localhost:9002/accounts/root/followers" | ||
11 | ], | ||
12 | "cc": [] | ||
13 | } | ||
diff --git a/server/tests/api/activitypub/json/peertube/invalid-keys.json b/server/tests/api/activitypub/json/peertube/invalid-keys.json new file mode 100644 index 000000000..0544e96b9 --- /dev/null +++ b/server/tests/api/activitypub/json/peertube/invalid-keys.json | |||
@@ -0,0 +1,6 @@ | |||
1 | { | ||
2 | "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw2Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n", | ||
3 | "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----" | ||
4 | } | ||
5 | |||
6 | |||
diff --git a/server/tests/api/activitypub/json/peertube/keys.json b/server/tests/api/activitypub/json/peertube/keys.json new file mode 100644 index 000000000..1a7700865 --- /dev/null +++ b/server/tests/api/activitypub/json/peertube/keys.json | |||
@@ -0,0 +1,4 @@ | |||
1 | { | ||
2 | "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n", | ||
3 | "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----" | ||
4 | } | ||
diff --git a/server/tests/api/activitypub/refresher.ts b/server/tests/api/activitypub/refresher.ts new file mode 100644 index 000000000..62ad8a0b5 --- /dev/null +++ b/server/tests/api/activitypub/refresher.ts | |||
@@ -0,0 +1,93 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { | ||
5 | doubleFollow, | ||
6 | flushAndRunMultipleServers, | ||
7 | getVideo, | ||
8 | killallServers, | ||
9 | reRunServer, | ||
10 | ServerInfo, | ||
11 | setAccessTokensToServers, | ||
12 | uploadVideo, | ||
13 | wait, | ||
14 | setVideoField, | ||
15 | waitJobs | ||
16 | } from '../../../../shared/utils' | ||
17 | |||
18 | describe('Test AP refresher', function () { | ||
19 | let servers: ServerInfo[] = [] | ||
20 | let videoUUID1: string | ||
21 | let videoUUID2: string | ||
22 | let videoUUID3: string | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(60000) | ||
26 | |||
27 | servers = await flushAndRunMultipleServers(2) | ||
28 | |||
29 | // Get the access tokens | ||
30 | await setAccessTokensToServers(servers) | ||
31 | |||
32 | { | ||
33 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' }) | ||
34 | videoUUID1 = res.body.video.uuid | ||
35 | } | ||
36 | |||
37 | { | ||
38 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' }) | ||
39 | videoUUID2 = res.body.video.uuid | ||
40 | } | ||
41 | |||
42 | { | ||
43 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video3' }) | ||
44 | videoUUID3 = res.body.video.uuid | ||
45 | } | ||
46 | |||
47 | await doubleFollow(servers[0], servers[1]) | ||
48 | }) | ||
49 | |||
50 | it('Should remove a deleted remote video', async function () { | ||
51 | this.timeout(60000) | ||
52 | |||
53 | await wait(10000) | ||
54 | |||
55 | // Change UUID so the remote server returns a 404 | ||
56 | await setVideoField(2, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f') | ||
57 | |||
58 | await getVideo(servers[0].url, videoUUID1) | ||
59 | await getVideo(servers[0].url, videoUUID2) | ||
60 | |||
61 | await waitJobs(servers) | ||
62 | |||
63 | await getVideo(servers[0].url, videoUUID1, 404) | ||
64 | await getVideo(servers[0].url, videoUUID2, 200) | ||
65 | }) | ||
66 | |||
67 | it('Should not update a remote video if the remote instance is down', async function () { | ||
68 | this.timeout(60000) | ||
69 | |||
70 | killallServers([ servers[1] ]) | ||
71 | |||
72 | await setVideoField(2, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e') | ||
73 | |||
74 | // Video will need a refresh | ||
75 | await wait(10000) | ||
76 | |||
77 | await getVideo(servers[0].url, videoUUID3) | ||
78 | // The refresh should fail | ||
79 | await waitJobs([ servers[0] ]) | ||
80 | |||
81 | await reRunServer(servers[1]) | ||
82 | |||
83 | // Should not refresh the video, even if the last refresh failed (to avoir a loop on dead instances) | ||
84 | await getVideo(servers[0].url, videoUUID3) | ||
85 | await waitJobs(servers) | ||
86 | |||
87 | await getVideo(servers[0].url, videoUUID3, 200) | ||
88 | }) | ||
89 | |||
90 | after(async function () { | ||
91 | killallServers(servers) | ||
92 | }) | ||
93 | }) | ||
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts new file mode 100644 index 000000000..342ae0fa1 --- /dev/null +++ b/server/tests/api/activitypub/security.ts | |||
@@ -0,0 +1,187 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | |||
5 | import { | ||
6 | flushAndRunMultipleServers, | ||
7 | flushTests, | ||
8 | killallServers, | ||
9 | makeFollowRequest, | ||
10 | makePOSTAPRequest, | ||
11 | ServerInfo, | ||
12 | setActorField | ||
13 | } from '../../../../shared/utils' | ||
14 | import { HTTP_SIGNATURE } from '../../../initializers' | ||
15 | import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' | ||
16 | import * as chai from 'chai' | ||
17 | import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' | ||
18 | |||
19 | const expect = chai.expect | ||
20 | |||
21 | function setKeysOfServer2 (serverNumber: number, publicKey: string, privateKey: string) { | ||
22 | return Promise.all([ | ||
23 | setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'publicKey', publicKey), | ||
24 | setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'privateKey', privateKey) | ||
25 | ]) | ||
26 | } | ||
27 | |||
28 | function setKeysOfServer3 (serverNumber: number, publicKey: string, privateKey: string) { | ||
29 | return Promise.all([ | ||
30 | setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'publicKey', publicKey), | ||
31 | setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'privateKey', privateKey) | ||
32 | ]) | ||
33 | } | ||
34 | |||
35 | describe('Test ActivityPub security', function () { | ||
36 | let servers: ServerInfo[] | ||
37 | let url: string | ||
38 | |||
39 | const keys = require('./json/peertube/keys.json') | ||
40 | const invalidKeys = require('./json/peertube/invalid-keys.json') | ||
41 | const baseHttpSignature = { | ||
42 | algorithm: HTTP_SIGNATURE.ALGORITHM, | ||
43 | authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, | ||
44 | keyId: 'acct:peertube@localhost:9002', | ||
45 | key: keys.privateKey, | ||
46 | headers: HTTP_SIGNATURE.HEADERS_TO_SIGN | ||
47 | } | ||
48 | |||
49 | // --------------------------------------------------------------- | ||
50 | |||
51 | before(async function () { | ||
52 | this.timeout(60000) | ||
53 | |||
54 | servers = await flushAndRunMultipleServers(3) | ||
55 | |||
56 | url = servers[0].url + '/inbox' | ||
57 | |||
58 | await setKeysOfServer2(1, keys.publicKey, keys.privateKey) | ||
59 | |||
60 | const to = { url: 'http://localhost:9001/accounts/peertube' } | ||
61 | const by = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } | ||
62 | await makeFollowRequest(to, by) | ||
63 | }) | ||
64 | |||
65 | describe('When checking HTTP signature', function () { | ||
66 | |||
67 | it('Should fail with an invalid digest', async function () { | ||
68 | const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) | ||
69 | const headers = { | ||
70 | Digest: buildDigest({ hello: 'coucou' }) | ||
71 | } | ||
72 | |||
73 | const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers) | ||
74 | |||
75 | expect(response.statusCode).to.equal(403) | ||
76 | }) | ||
77 | |||
78 | it('Should fail with an invalid date', async function () { | ||
79 | const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) | ||
80 | const headers = buildGlobalHeaders(body) | ||
81 | headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' | ||
82 | |||
83 | const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers) | ||
84 | |||
85 | expect(response.statusCode).to.equal(403) | ||
86 | }) | ||
87 | |||
88 | it('Should fail with bad keys', async function () { | ||
89 | await setKeysOfServer2(1, invalidKeys.publicKey, invalidKeys.privateKey) | ||
90 | await setKeysOfServer2(2, invalidKeys.publicKey, invalidKeys.privateKey) | ||
91 | |||
92 | const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) | ||
93 | const headers = buildGlobalHeaders(body) | ||
94 | |||
95 | const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers) | ||
96 | |||
97 | expect(response.statusCode).to.equal(403) | ||
98 | }) | ||
99 | |||
100 | it('Should succeed with a valid HTTP signature', async function () { | ||
101 | await setKeysOfServer2(1, keys.publicKey, keys.privateKey) | ||
102 | await setKeysOfServer2(2, keys.publicKey, keys.privateKey) | ||
103 | |||
104 | const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) | ||
105 | const headers = buildGlobalHeaders(body) | ||
106 | |||
107 | const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers) | ||
108 | |||
109 | expect(response.statusCode).to.equal(204) | ||
110 | }) | ||
111 | }) | ||
112 | |||
113 | describe('When checking Linked Data Signature', function () { | ||
114 | before(async () => { | ||
115 | await setKeysOfServer3(3, keys.publicKey, keys.privateKey) | ||
116 | |||
117 | const to = { url: 'http://localhost:9001/accounts/peertube' } | ||
118 | const by = { url: 'http://localhost:9003/accounts/peertube', privateKey: keys.privateKey } | ||
119 | await makeFollowRequest(to, by) | ||
120 | }) | ||
121 | |||
122 | it('Should fail with bad keys', async function () { | ||
123 | this.timeout(10000) | ||
124 | |||
125 | await setKeysOfServer3(1, invalidKeys.publicKey, invalidKeys.privateKey) | ||
126 | await setKeysOfServer3(3, invalidKeys.publicKey, invalidKeys.privateKey) | ||
127 | |||
128 | const body = require('./json/peertube/announce-without-context.json') | ||
129 | body.actor = 'http://localhost:9003/accounts/peertube' | ||
130 | |||
131 | const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:9003/accounts/peertube' } | ||
132 | const signedBody = await buildSignedActivity(signer, body) | ||
133 | |||
134 | const headers = buildGlobalHeaders(signedBody) | ||
135 | |||
136 | const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers) | ||
137 | |||
138 | expect(response.statusCode).to.equal(403) | ||
139 | }) | ||
140 | |||
141 | it('Should fail with an altered body', async function () { | ||
142 | this.timeout(10000) | ||
143 | |||
144 | await setKeysOfServer3(1, keys.publicKey, keys.privateKey) | ||
145 | await setKeysOfServer3(3, keys.publicKey, keys.privateKey) | ||
146 | |||
147 | const body = require('./json/peertube/announce-without-context.json') | ||
148 | body.actor = 'http://localhost:9003/accounts/peertube' | ||
149 | |||
150 | const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' } | ||
151 | const signedBody = await buildSignedActivity(signer, body) | ||
152 | |||
153 | signedBody.actor = 'http://localhost:9003/account/peertube' | ||
154 | |||
155 | const headers = buildGlobalHeaders(signedBody) | ||
156 | |||
157 | const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers) | ||
158 | |||
159 | expect(response.statusCode).to.equal(403) | ||
160 | }) | ||
161 | |||
162 | it('Should succeed with a valid signature', async function () { | ||
163 | this.timeout(10000) | ||
164 | |||
165 | const body = require('./json/peertube/announce-without-context.json') | ||
166 | body.actor = 'http://localhost:9003/accounts/peertube' | ||
167 | |||
168 | const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' } | ||
169 | const signedBody = await buildSignedActivity(signer, body) | ||
170 | |||
171 | const headers = buildGlobalHeaders(signedBody) | ||
172 | |||
173 | const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers) | ||
174 | |||
175 | expect(response.statusCode).to.equal(204) | ||
176 | }) | ||
177 | }) | ||
178 | |||
179 | after(async function () { | ||
180 | killallServers(servers) | ||
181 | |||
182 | // Keep the logs if the test failed | ||
183 | if (this['ok']) { | ||
184 | await flushTests() | ||
185 | } | ||
186 | }) | ||
187 | }) | ||
diff --git a/server/tests/api/check-params/accounts.ts b/server/tests/api/check-params/accounts.ts index 9e0b1e35c..68f9519c6 100644 --- a/server/tests/api/check-params/accounts.ts +++ b/server/tests/api/check-params/accounts.ts | |||
@@ -2,11 +2,15 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | 4 | ||
5 | import { flushTests, killallServers, runServer, ServerInfo } from '../../utils' | 5 | import { flushTests, killallServers, runServer, ServerInfo } from '../../../../shared/utils' |
6 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 6 | import { |
7 | import { getAccount } from '../../utils/users/accounts' | 7 | checkBadCountPagination, |
8 | 8 | checkBadSortPagination, | |
9 | describe('Test users API validators', function () { | 9 | checkBadStartPagination |
10 | } from '../../../../shared/utils/requests/check-api-params' | ||
11 | import { getAccount } from '../../../../shared/utils/users/accounts' | ||
12 | |||
13 | describe('Test accounts API validators', function () { | ||
10 | const path = '/api/v1/accounts/' | 14 | const path = '/api/v1/accounts/' |
11 | let server: ServerInfo | 15 | let server: ServerInfo |
12 | 16 | ||
diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts new file mode 100644 index 000000000..c20453c16 --- /dev/null +++ b/server/tests/api/check-params/blocklist.ts | |||
@@ -0,0 +1,498 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | |||
5 | import { | ||
6 | createUser, | ||
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
9 | flushTests, | ||
10 | killallServers, | ||
11 | makeDeleteRequest, | ||
12 | makeGetRequest, | ||
13 | makePostBodyRequest, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, userLogin | ||
16 | } from '../../../../shared/utils' | ||
17 | import { | ||
18 | checkBadCountPagination, | ||
19 | checkBadSortPagination, | ||
20 | checkBadStartPagination | ||
21 | } from '../../../../shared/utils/requests/check-api-params' | ||
22 | |||
23 | describe('Test blocklist API validators', function () { | ||
24 | let servers: ServerInfo[] | ||
25 | let server: ServerInfo | ||
26 | let userAccessToken: string | ||
27 | |||
28 | before(async function () { | ||
29 | this.timeout(60000) | ||
30 | |||
31 | await flushTests() | ||
32 | |||
33 | servers = await flushAndRunMultipleServers(2) | ||
34 | await setAccessTokensToServers(servers) | ||
35 | |||
36 | server = servers[0] | ||
37 | |||
38 | const user = { username: 'user1', password: 'password' } | ||
39 | await createUser(server.url, server.accessToken, user.username, user.password) | ||
40 | |||
41 | userAccessToken = await userLogin(server, user) | ||
42 | |||
43 | await doubleFollow(servers[0], servers[1]) | ||
44 | }) | ||
45 | |||
46 | // --------------------------------------------------------------- | ||
47 | |||
48 | describe('When managing user blocklist', function () { | ||
49 | |||
50 | describe('When managing user accounts blocklist', function () { | ||
51 | const path = '/api/v1/users/me/blocklist/accounts' | ||
52 | |||
53 | describe('When listing blocked accounts', function () { | ||
54 | it('Should fail with an unauthenticated user', async function () { | ||
55 | await makeGetRequest({ | ||
56 | url: server.url, | ||
57 | path, | ||
58 | statusCodeExpected: 401 | ||
59 | }) | ||
60 | }) | ||
61 | |||
62 | it('Should fail with a bad start pagination', async function () { | ||
63 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
64 | }) | ||
65 | |||
66 | it('Should fail with a bad count pagination', async function () { | ||
67 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
68 | }) | ||
69 | |||
70 | it('Should fail with an incorrect sort', async function () { | ||
71 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
72 | }) | ||
73 | }) | ||
74 | |||
75 | describe('When blocking an account', function () { | ||
76 | it('Should fail with an unauthenticated user', async function () { | ||
77 | await makePostBodyRequest({ | ||
78 | url: server.url, | ||
79 | path, | ||
80 | fields: { accountName: 'user1' }, | ||
81 | statusCodeExpected: 401 | ||
82 | }) | ||
83 | }) | ||
84 | |||
85 | it('Should fail with an unknown account', async function () { | ||
86 | await makePostBodyRequest({ | ||
87 | url: server.url, | ||
88 | token: server.accessToken, | ||
89 | path, | ||
90 | fields: { accountName: 'user2' }, | ||
91 | statusCodeExpected: 404 | ||
92 | }) | ||
93 | }) | ||
94 | |||
95 | it('Should fail to block ourselves', async function () { | ||
96 | await makePostBodyRequest({ | ||
97 | url: server.url, | ||
98 | token: server.accessToken, | ||
99 | path, | ||
100 | fields: { accountName: 'root' }, | ||
101 | statusCodeExpected: 409 | ||
102 | }) | ||
103 | }) | ||
104 | |||
105 | it('Should succeed with the correct params', async function () { | ||
106 | await makePostBodyRequest({ | ||
107 | url: server.url, | ||
108 | token: server.accessToken, | ||
109 | path, | ||
110 | fields: { accountName: 'user1' }, | ||
111 | statusCodeExpected: 204 | ||
112 | }) | ||
113 | }) | ||
114 | }) | ||
115 | |||
116 | describe('When unblocking an account', function () { | ||
117 | it('Should fail with an unauthenticated user', async function () { | ||
118 | await makeDeleteRequest({ | ||
119 | url: server.url, | ||
120 | path: path + '/user1', | ||
121 | statusCodeExpected: 401 | ||
122 | }) | ||
123 | }) | ||
124 | |||
125 | it('Should fail with an unknown account block', async function () { | ||
126 | await makeDeleteRequest({ | ||
127 | url: server.url, | ||
128 | path: path + '/user2', | ||
129 | token: server.accessToken, | ||
130 | statusCodeExpected: 404 | ||
131 | }) | ||
132 | }) | ||
133 | |||
134 | it('Should succeed with the correct params', async function () { | ||
135 | await makeDeleteRequest({ | ||
136 | url: server.url, | ||
137 | path: path + '/user1', | ||
138 | token: server.accessToken, | ||
139 | statusCodeExpected: 204 | ||
140 | }) | ||
141 | }) | ||
142 | }) | ||
143 | }) | ||
144 | |||
145 | describe('When managing user servers blocklist', function () { | ||
146 | const path = '/api/v1/users/me/blocklist/servers' | ||
147 | |||
148 | describe('When listing blocked servers', function () { | ||
149 | it('Should fail with an unauthenticated user', async function () { | ||
150 | await makeGetRequest({ | ||
151 | url: server.url, | ||
152 | path, | ||
153 | statusCodeExpected: 401 | ||
154 | }) | ||
155 | }) | ||
156 | |||
157 | it('Should fail with a bad start pagination', async function () { | ||
158 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
159 | }) | ||
160 | |||
161 | it('Should fail with a bad count pagination', async function () { | ||
162 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
163 | }) | ||
164 | |||
165 | it('Should fail with an incorrect sort', async function () { | ||
166 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
167 | }) | ||
168 | }) | ||
169 | |||
170 | describe('When blocking a server', function () { | ||
171 | it('Should fail with an unauthenticated user', async function () { | ||
172 | await makePostBodyRequest({ | ||
173 | url: server.url, | ||
174 | path, | ||
175 | fields: { host: 'localhost:9002' }, | ||
176 | statusCodeExpected: 401 | ||
177 | }) | ||
178 | }) | ||
179 | |||
180 | it('Should fail with an unknown server', async function () { | ||
181 | await makePostBodyRequest({ | ||
182 | url: server.url, | ||
183 | token: server.accessToken, | ||
184 | path, | ||
185 | fields: { host: 'localhost:9003' }, | ||
186 | statusCodeExpected: 404 | ||
187 | }) | ||
188 | }) | ||
189 | |||
190 | it('Should fail with our own server', async function () { | ||
191 | await makePostBodyRequest({ | ||
192 | url: server.url, | ||
193 | token: server.accessToken, | ||
194 | path, | ||
195 | fields: { host: 'localhost:9001' }, | ||
196 | statusCodeExpected: 409 | ||
197 | }) | ||
198 | }) | ||
199 | |||
200 | it('Should succeed with the correct params', async function () { | ||
201 | await makePostBodyRequest({ | ||
202 | url: server.url, | ||
203 | token: server.accessToken, | ||
204 | path, | ||
205 | fields: { host: 'localhost:9002' }, | ||
206 | statusCodeExpected: 204 | ||
207 | }) | ||
208 | }) | ||
209 | }) | ||
210 | |||
211 | describe('When unblocking a server', function () { | ||
212 | it('Should fail with an unauthenticated user', async function () { | ||
213 | await makeDeleteRequest({ | ||
214 | url: server.url, | ||
215 | path: path + '/localhost:9002', | ||
216 | statusCodeExpected: 401 | ||
217 | }) | ||
218 | }) | ||
219 | |||
220 | it('Should fail with an unknown server block', async function () { | ||
221 | await makeDeleteRequest({ | ||
222 | url: server.url, | ||
223 | path: path + '/localhost:9003', | ||
224 | token: server.accessToken, | ||
225 | statusCodeExpected: 404 | ||
226 | }) | ||
227 | }) | ||
228 | |||
229 | it('Should succeed with the correct params', async function () { | ||
230 | await makeDeleteRequest({ | ||
231 | url: server.url, | ||
232 | path: path + '/localhost:9002', | ||
233 | token: server.accessToken, | ||
234 | statusCodeExpected: 204 | ||
235 | }) | ||
236 | }) | ||
237 | }) | ||
238 | }) | ||
239 | }) | ||
240 | |||
241 | describe('When managing server blocklist', function () { | ||
242 | |||
243 | describe('When managing server accounts blocklist', function () { | ||
244 | const path = '/api/v1/server/blocklist/accounts' | ||
245 | |||
246 | describe('When listing blocked accounts', function () { | ||
247 | it('Should fail with an unauthenticated user', async function () { | ||
248 | await makeGetRequest({ | ||
249 | url: server.url, | ||
250 | path, | ||
251 | statusCodeExpected: 401 | ||
252 | }) | ||
253 | }) | ||
254 | |||
255 | it('Should fail with a user without the appropriate rights', async function () { | ||
256 | await makeGetRequest({ | ||
257 | url: server.url, | ||
258 | token: userAccessToken, | ||
259 | path, | ||
260 | statusCodeExpected: 403 | ||
261 | }) | ||
262 | }) | ||
263 | |||
264 | it('Should fail with a bad start pagination', async function () { | ||
265 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
266 | }) | ||
267 | |||
268 | it('Should fail with a bad count pagination', async function () { | ||
269 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
270 | }) | ||
271 | |||
272 | it('Should fail with an incorrect sort', async function () { | ||
273 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
274 | }) | ||
275 | }) | ||
276 | |||
277 | describe('When blocking an account', function () { | ||
278 | it('Should fail with an unauthenticated user', async function () { | ||
279 | await makePostBodyRequest({ | ||
280 | url: server.url, | ||
281 | path, | ||
282 | fields: { accountName: 'user1' }, | ||
283 | statusCodeExpected: 401 | ||
284 | }) | ||
285 | }) | ||
286 | |||
287 | it('Should fail with a user without the appropriate rights', async function () { | ||
288 | await makePostBodyRequest({ | ||
289 | url: server.url, | ||
290 | token: userAccessToken, | ||
291 | path, | ||
292 | fields: { accountName: 'user1' }, | ||
293 | statusCodeExpected: 403 | ||
294 | }) | ||
295 | }) | ||
296 | |||
297 | it('Should fail with an unknown account', async function () { | ||
298 | await makePostBodyRequest({ | ||
299 | url: server.url, | ||
300 | token: server.accessToken, | ||
301 | path, | ||
302 | fields: { accountName: 'user2' }, | ||
303 | statusCodeExpected: 404 | ||
304 | }) | ||
305 | }) | ||
306 | |||
307 | it('Should fail to block ourselves', async function () { | ||
308 | await makePostBodyRequest({ | ||
309 | url: server.url, | ||
310 | token: server.accessToken, | ||
311 | path, | ||
312 | fields: { accountName: 'root' }, | ||
313 | statusCodeExpected: 409 | ||
314 | }) | ||
315 | }) | ||
316 | |||
317 | it('Should succeed with the correct params', async function () { | ||
318 | await makePostBodyRequest({ | ||
319 | url: server.url, | ||
320 | token: server.accessToken, | ||
321 | path, | ||
322 | fields: { accountName: 'user1' }, | ||
323 | statusCodeExpected: 204 | ||
324 | }) | ||
325 | }) | ||
326 | }) | ||
327 | |||
328 | describe('When unblocking an account', function () { | ||
329 | it('Should fail with an unauthenticated user', async function () { | ||
330 | await makeDeleteRequest({ | ||
331 | url: server.url, | ||
332 | path: path + '/user1', | ||
333 | statusCodeExpected: 401 | ||
334 | }) | ||
335 | }) | ||
336 | |||
337 | it('Should fail with a user without the appropriate rights', async function () { | ||
338 | await makeDeleteRequest({ | ||
339 | url: server.url, | ||
340 | path: path + '/user1', | ||
341 | token: userAccessToken, | ||
342 | statusCodeExpected: 403 | ||
343 | }) | ||
344 | }) | ||
345 | |||
346 | it('Should fail with an unknown account block', async function () { | ||
347 | await makeDeleteRequest({ | ||
348 | url: server.url, | ||
349 | path: path + '/user2', | ||
350 | token: server.accessToken, | ||
351 | statusCodeExpected: 404 | ||
352 | }) | ||
353 | }) | ||
354 | |||
355 | it('Should succeed with the correct params', async function () { | ||
356 | await makeDeleteRequest({ | ||
357 | url: server.url, | ||
358 | path: path + '/user1', | ||
359 | token: server.accessToken, | ||
360 | statusCodeExpected: 204 | ||
361 | }) | ||
362 | }) | ||
363 | }) | ||
364 | }) | ||
365 | |||
366 | describe('When managing server servers blocklist', function () { | ||
367 | const path = '/api/v1/server/blocklist/servers' | ||
368 | |||
369 | describe('When listing blocked servers', function () { | ||
370 | it('Should fail with an unauthenticated user', async function () { | ||
371 | await makeGetRequest({ | ||
372 | url: server.url, | ||
373 | path, | ||
374 | statusCodeExpected: 401 | ||
375 | }) | ||
376 | }) | ||
377 | |||
378 | it('Should fail with a user without the appropriate rights', async function () { | ||
379 | await makeGetRequest({ | ||
380 | url: server.url, | ||
381 | token: userAccessToken, | ||
382 | path, | ||
383 | statusCodeExpected: 403 | ||
384 | }) | ||
385 | }) | ||
386 | |||
387 | it('Should fail with a bad start pagination', async function () { | ||
388 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
389 | }) | ||
390 | |||
391 | it('Should fail with a bad count pagination', async function () { | ||
392 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
393 | }) | ||
394 | |||
395 | it('Should fail with an incorrect sort', async function () { | ||
396 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
397 | }) | ||
398 | }) | ||
399 | |||
400 | describe('When blocking a server', function () { | ||
401 | it('Should fail with an unauthenticated user', async function () { | ||
402 | await makePostBodyRequest({ | ||
403 | url: server.url, | ||
404 | path, | ||
405 | fields: { host: 'localhost:9002' }, | ||
406 | statusCodeExpected: 401 | ||
407 | }) | ||
408 | }) | ||
409 | |||
410 | it('Should fail with a user without the appropriate rights', async function () { | ||
411 | await makePostBodyRequest({ | ||
412 | url: server.url, | ||
413 | token: userAccessToken, | ||
414 | path, | ||
415 | fields: { host: 'localhost:9002' }, | ||
416 | statusCodeExpected: 403 | ||
417 | }) | ||
418 | }) | ||
419 | |||
420 | it('Should fail with an unknown server', async function () { | ||
421 | await makePostBodyRequest({ | ||
422 | url: server.url, | ||
423 | token: server.accessToken, | ||
424 | path, | ||
425 | fields: { host: 'localhost:9003' }, | ||
426 | statusCodeExpected: 404 | ||
427 | }) | ||
428 | }) | ||
429 | |||
430 | it('Should fail with our own server', async function () { | ||
431 | await makePostBodyRequest({ | ||
432 | url: server.url, | ||
433 | token: server.accessToken, | ||
434 | path, | ||
435 | fields: { host: 'localhost:9001' }, | ||
436 | statusCodeExpected: 409 | ||
437 | }) | ||
438 | }) | ||
439 | |||
440 | it('Should succeed with the correct params', async function () { | ||
441 | await makePostBodyRequest({ | ||
442 | url: server.url, | ||
443 | token: server.accessToken, | ||
444 | path, | ||
445 | fields: { host: 'localhost:9002' }, | ||
446 | statusCodeExpected: 204 | ||
447 | }) | ||
448 | }) | ||
449 | }) | ||
450 | |||
451 | describe('When unblocking a server', function () { | ||
452 | it('Should fail with an unauthenticated user', async function () { | ||
453 | await makeDeleteRequest({ | ||
454 | url: server.url, | ||
455 | path: path + '/localhost:9002', | ||
456 | statusCodeExpected: 401 | ||
457 | }) | ||
458 | }) | ||
459 | |||
460 | it('Should fail with a user without the appropriate rights', async function () { | ||
461 | await makeDeleteRequest({ | ||
462 | url: server.url, | ||
463 | path: path + '/localhost:9002', | ||
464 | token: userAccessToken, | ||
465 | statusCodeExpected: 403 | ||
466 | }) | ||
467 | }) | ||
468 | |||
469 | it('Should fail with an unknown server block', async function () { | ||
470 | await makeDeleteRequest({ | ||
471 | url: server.url, | ||
472 | path: path + '/localhost:9003', | ||
473 | token: server.accessToken, | ||
474 | statusCodeExpected: 404 | ||
475 | }) | ||
476 | }) | ||
477 | |||
478 | it('Should succeed with the correct params', async function () { | ||
479 | await makeDeleteRequest({ | ||
480 | url: server.url, | ||
481 | path: path + '/localhost:9002', | ||
482 | token: server.accessToken, | ||
483 | statusCodeExpected: 204 | ||
484 | }) | ||
485 | }) | ||
486 | }) | ||
487 | }) | ||
488 | }) | ||
489 | |||
490 | after(async function () { | ||
491 | killallServers(servers) | ||
492 | |||
493 | // Keep the logs if the test failed | ||
494 | if (this['ok']) { | ||
495 | await flushTests() | ||
496 | } | ||
497 | }) | ||
498 | }) | ||
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index d807f910b..07de2b5a5 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -7,7 +7,7 @@ import { CustomConfig } from '../../../../shared/models/server/custom-config.mod | |||
7 | import { | 7 | import { |
8 | createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo, | 8 | createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo, |
9 | setAccessTokensToServers, userLogin, immutableAssign | 9 | setAccessTokensToServers, userLogin, immutableAssign |
10 | } from '../../utils' | 10 | } from '../../../../shared/utils' |
11 | 11 | ||
12 | describe('Test config API validators', function () { | 12 | describe('Test config API validators', function () { |
13 | const path = '/api/v1/config/custom' | 13 | const path = '/api/v1/config/custom' |
@@ -48,12 +48,16 @@ describe('Test config API validators', function () { | |||
48 | admin: { | 48 | admin: { |
49 | email: 'superadmin1@example.com' | 49 | email: 'superadmin1@example.com' |
50 | }, | 50 | }, |
51 | contactForm: { | ||
52 | enabled: false | ||
53 | }, | ||
51 | user: { | 54 | user: { |
52 | videoQuota: 5242881, | 55 | videoQuota: 5242881, |
53 | videoQuotaDaily: 318742 | 56 | videoQuotaDaily: 318742 |
54 | }, | 57 | }, |
55 | transcoding: { | 58 | transcoding: { |
56 | enabled: true, | 59 | enabled: true, |
60 | allowAdditionalExtensions: true, | ||
57 | threads: 1, | 61 | threads: 1, |
58 | resolutions: { | 62 | resolutions: { |
59 | '240p': false, | 63 | '240p': false, |
@@ -61,6 +65,9 @@ describe('Test config API validators', function () { | |||
61 | '480p': true, | 65 | '480p': true, |
62 | '720p': false, | 66 | '720p': false, |
63 | '1080p': false | 67 | '1080p': false |
68 | }, | ||
69 | hls: { | ||
70 | enabled: false | ||
64 | } | 71 | } |
65 | }, | 72 | }, |
66 | import: { | 73 | import: { |
diff --git a/server/tests/api/check-params/contact-form.ts b/server/tests/api/check-params/contact-form.ts new file mode 100644 index 000000000..c7e014b1f --- /dev/null +++ b/server/tests/api/check-params/contact-form.ts | |||
@@ -0,0 +1,96 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | |||
5 | import { | ||
6 | flushTests, | ||
7 | immutableAssign, | ||
8 | killallServers, | ||
9 | reRunServer, | ||
10 | runServer, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers | ||
13 | } from '../../../../shared/utils' | ||
14 | import { | ||
15 | checkBadCountPagination, | ||
16 | checkBadSortPagination, | ||
17 | checkBadStartPagination | ||
18 | } from '../../../../shared/utils/requests/check-api-params' | ||
19 | import { getAccount } from '../../../../shared/utils/users/accounts' | ||
20 | import { sendContactForm } from '../../../../shared/utils/server/contact-form' | ||
21 | import { MockSmtpServer } from '../../../../shared/utils/miscs/email' | ||
22 | |||
23 | describe('Test contact form API validators', function () { | ||
24 | let server: ServerInfo | ||
25 | const emails: object[] = [] | ||
26 | const defaultBody = { | ||
27 | fromName: 'super name', | ||
28 | fromEmail: 'toto@example.com', | ||
29 | body: 'Hello, how are you?' | ||
30 | } | ||
31 | |||
32 | // --------------------------------------------------------------- | ||
33 | |||
34 | before(async function () { | ||
35 | this.timeout(60000) | ||
36 | |||
37 | await flushTests() | ||
38 | await MockSmtpServer.Instance.collectEmails(emails) | ||
39 | |||
40 | // Email is disabled | ||
41 | server = await runServer(1) | ||
42 | }) | ||
43 | |||
44 | it('Should not accept a contact form if emails are disabled', async function () { | ||
45 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 409 })) | ||
46 | }) | ||
47 | |||
48 | it('Should not accept a contact form if it is disabled in the configuration', async function () { | ||
49 | this.timeout(10000) | ||
50 | |||
51 | killallServers([ server ]) | ||
52 | |||
53 | // Contact form is disabled | ||
54 | await reRunServer(server, { smtp: { hostname: 'localhost' }, contact_form: { enabled: false } }) | ||
55 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 409 })) | ||
56 | }) | ||
57 | |||
58 | it('Should not accept a contact form if from email is invalid', async function () { | ||
59 | this.timeout(10000) | ||
60 | |||
61 | killallServers([ server ]) | ||
62 | |||
63 | // Email & contact form enabled | ||
64 | await reRunServer(server, { smtp: { hostname: 'localhost' } }) | ||
65 | |||
66 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: 'badEmail' })) | ||
67 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: 'badEmail@' })) | ||
68 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: undefined })) | ||
69 | }) | ||
70 | |||
71 | it('Should not accept a contact form if from name is invalid', async function () { | ||
72 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromName: 'name'.repeat(100) })) | ||
73 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromName: '' })) | ||
74 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromName: undefined })) | ||
75 | }) | ||
76 | |||
77 | it('Should not accept a contact form if body is invalid', async function () { | ||
78 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, body: 'body'.repeat(5000) })) | ||
79 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, body: 'a' })) | ||
80 | await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, body: undefined })) | ||
81 | }) | ||
82 | |||
83 | it('Should accept a contact form with the correct parameters', async function () { | ||
84 | await sendContactForm(immutableAssign(defaultBody, { url: server.url })) | ||
85 | }) | ||
86 | |||
87 | after(async function () { | ||
88 | MockSmtpServer.Instance.kill() | ||
89 | killallServers([ server ]) | ||
90 | |||
91 | // Keep the logs if the test failed | ||
92 | if (this['ok']) { | ||
93 | await flushTests() | ||
94 | } | ||
95 | }) | ||
96 | }) | ||
diff --git a/server/tests/api/check-params/follows.ts b/server/tests/api/check-params/follows.ts index cdc95c81a..2ad1575a3 100644 --- a/server/tests/api/check-params/follows.ts +++ b/server/tests/api/check-params/follows.ts | |||
@@ -5,8 +5,12 @@ import 'mocha' | |||
5 | import { | 5 | import { |
6 | createUser, flushTests, killallServers, makeDeleteRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, | 6 | createUser, flushTests, killallServers, makeDeleteRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, |
7 | userLogin | 7 | userLogin |
8 | } from '../../utils' | 8 | } from '../../../../shared/utils' |
9 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 9 | import { |
10 | checkBadCountPagination, | ||
11 | checkBadSortPagination, | ||
12 | checkBadStartPagination | ||
13 | } from '../../../../shared/utils/requests/check-api-params' | ||
10 | 14 | ||
11 | describe('Test server follows API validators', function () { | 15 | describe('Test server follows API validators', function () { |
12 | let server: ServerInfo | 16 | let server: ServerInfo |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 71a217649..77c17036a 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -1,11 +1,13 @@ | |||
1 | // Order of the tests we want to execute | ||
2 | import './accounts' | 1 | import './accounts' |
2 | import './blocklist' | ||
3 | import './config' | 3 | import './config' |
4 | import './contact-form' | ||
4 | import './follows' | 5 | import './follows' |
5 | import './jobs' | 6 | import './jobs' |
6 | import './redundancy' | 7 | import './redundancy' |
7 | import './search' | 8 | import './search' |
8 | import './services' | 9 | import './services' |
10 | import './user-notifications' | ||
9 | import './user-subscriptions' | 11 | import './user-subscriptions' |
10 | import './users' | 12 | import './users' |
11 | import './video-abuses' | 13 | import './video-abuses' |
@@ -15,4 +17,5 @@ import './video-channels' | |||
15 | import './video-comments' | 17 | import './video-comments' |
16 | import './video-imports' | 18 | import './video-imports' |
17 | import './videos' | 19 | import './videos' |
20 | import './videos-filter' | ||
18 | import './videos-history' | 21 | import './videos-history' |
diff --git a/server/tests/api/check-params/jobs.ts b/server/tests/api/check-params/jobs.ts index ce3ac8809..89760ff98 100644 --- a/server/tests/api/check-params/jobs.ts +++ b/server/tests/api/check-params/jobs.ts | |||
@@ -2,9 +2,21 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | 4 | ||
5 | import { createUser, flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, userLogin } from '../../utils' | 5 | import { |
6 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 6 | createUser, |
7 | import { makeGetRequest } from '../../utils/requests/requests' | 7 | flushTests, |
8 | killallServers, | ||
9 | runServer, | ||
10 | ServerInfo, | ||
11 | setAccessTokensToServers, | ||
12 | userLogin | ||
13 | } from '../../../../shared/utils' | ||
14 | import { | ||
15 | checkBadCountPagination, | ||
16 | checkBadSortPagination, | ||
17 | checkBadStartPagination | ||
18 | } from '../../../../shared/utils/requests/check-api-params' | ||
19 | import { makeGetRequest } from '../../../../shared/utils/requests/requests' | ||
8 | 20 | ||
9 | describe('Test jobs API validators', function () { | 21 | describe('Test jobs API validators', function () { |
10 | const path = '/api/v1/jobs/failed' | 22 | const path = '/api/v1/jobs/failed' |
diff --git a/server/tests/api/check-params/redundancy.ts b/server/tests/api/check-params/redundancy.ts index aa588e3dd..ff4726ceb 100644 --- a/server/tests/api/check-params/redundancy.ts +++ b/server/tests/api/check-params/redundancy.ts | |||
@@ -12,7 +12,7 @@ import { | |||
12 | ServerInfo, | 12 | ServerInfo, |
13 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | userLogin | 14 | userLogin |
15 | } from '../../utils' | 15 | } from '../../../../shared/utils' |
16 | 16 | ||
17 | describe('Test server redundancy API validators', function () { | 17 | describe('Test server redundancy API validators', function () { |
18 | let servers: ServerInfo[] | 18 | let servers: ServerInfo[] |
diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts index eabf602ac..aa81965f3 100644 --- a/server/tests/api/check-params/search.ts +++ b/server/tests/api/check-params/search.ts | |||
@@ -2,8 +2,12 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | 4 | ||
5 | import { flushTests, immutableAssign, killallServers, makeGetRequest, runServer, ServerInfo } from '../../utils' | 5 | import { flushTests, immutableAssign, killallServers, makeGetRequest, runServer, ServerInfo } from '../../../../shared/utils' |
6 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 6 | import { |
7 | checkBadCountPagination, | ||
8 | checkBadSortPagination, | ||
9 | checkBadStartPagination | ||
10 | } from '../../../../shared/utils/requests/check-api-params' | ||
7 | 11 | ||
8 | describe('Test videos API validator', function () { | 12 | describe('Test videos API validator', function () { |
9 | let server: ServerInfo | 13 | let server: ServerInfo |
diff --git a/server/tests/api/check-params/services.ts b/server/tests/api/check-params/services.ts index fcde7e179..28591af9d 100644 --- a/server/tests/api/check-params/services.ts +++ b/server/tests/api/check-params/services.ts | |||
@@ -2,7 +2,15 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | 4 | ||
5 | import { flushTests, killallServers, makeGetRequest, runServer, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils' | 5 | import { |
6 | flushTests, | ||
7 | killallServers, | ||
8 | makeGetRequest, | ||
9 | runServer, | ||
10 | ServerInfo, | ||
11 | setAccessTokensToServers, | ||
12 | uploadVideo | ||
13 | } from '../../../../shared/utils' | ||
6 | 14 | ||
7 | describe('Test services API validators', function () { | 15 | describe('Test services API validators', function () { |
8 | let server: ServerInfo | 16 | let server: ServerInfo |
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts new file mode 100644 index 000000000..714f481e9 --- /dev/null +++ b/server/tests/api/check-params/user-notifications.ts | |||
@@ -0,0 +1,297 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as io from 'socket.io-client' | ||
5 | |||
6 | import { | ||
7 | flushTests, | ||
8 | immutableAssign, | ||
9 | killallServers, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | makePutBodyRequest, | ||
13 | runServer, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, | ||
16 | wait | ||
17 | } from '../../../../shared/utils' | ||
18 | import { | ||
19 | checkBadCountPagination, | ||
20 | checkBadSortPagination, | ||
21 | checkBadStartPagination | ||
22 | } from '../../../../shared/utils/requests/check-api-params' | ||
23 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../../shared/models/users' | ||
24 | |||
25 | describe('Test user notifications API validators', function () { | ||
26 | let server: ServerInfo | ||
27 | |||
28 | // --------------------------------------------------------------- | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(30000) | ||
32 | |||
33 | await flushTests() | ||
34 | |||
35 | server = await runServer(1) | ||
36 | |||
37 | await setAccessTokensToServers([ server ]) | ||
38 | }) | ||
39 | |||
40 | describe('When listing my notifications', function () { | ||
41 | const path = '/api/v1/users/me/notifications' | ||
42 | |||
43 | it('Should fail with a bad start pagination', async function () { | ||
44 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
45 | }) | ||
46 | |||
47 | it('Should fail with a bad count pagination', async function () { | ||
48 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
49 | }) | ||
50 | |||
51 | it('Should fail with an incorrect sort', async function () { | ||
52 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
53 | }) | ||
54 | |||
55 | it('Should fail with an incorrect unread parameter', async function () { | ||
56 | await makeGetRequest({ | ||
57 | url: server.url, | ||
58 | path, | ||
59 | query: { | ||
60 | unread: 'toto' | ||
61 | }, | ||
62 | token: server.accessToken, | ||
63 | statusCodeExpected: 200 | ||
64 | }) | ||
65 | }) | ||
66 | |||
67 | it('Should fail with a non authenticated user', async function () { | ||
68 | await makeGetRequest({ | ||
69 | url: server.url, | ||
70 | path, | ||
71 | statusCodeExpected: 401 | ||
72 | }) | ||
73 | }) | ||
74 | |||
75 | it('Should succeed with the correct parameters', async function () { | ||
76 | await makeGetRequest({ | ||
77 | url: server.url, | ||
78 | path, | ||
79 | token: server.accessToken, | ||
80 | statusCodeExpected: 200 | ||
81 | }) | ||
82 | }) | ||
83 | }) | ||
84 | |||
85 | describe('When marking as read my notifications', function () { | ||
86 | const path = '/api/v1/users/me/notifications/read' | ||
87 | |||
88 | it('Should fail with wrong ids parameters', async function () { | ||
89 | await makePostBodyRequest({ | ||
90 | url: server.url, | ||
91 | path, | ||
92 | fields: { | ||
93 | ids: [ 'hello' ] | ||
94 | }, | ||
95 | token: server.accessToken, | ||
96 | statusCodeExpected: 400 | ||
97 | }) | ||
98 | |||
99 | await makePostBodyRequest({ | ||
100 | url: server.url, | ||
101 | path, | ||
102 | fields: { | ||
103 | ids: [ ] | ||
104 | }, | ||
105 | token: server.accessToken, | ||
106 | statusCodeExpected: 400 | ||
107 | }) | ||
108 | |||
109 | await makePostBodyRequest({ | ||
110 | url: server.url, | ||
111 | path, | ||
112 | fields: { | ||
113 | ids: 5 | ||
114 | }, | ||
115 | token: server.accessToken, | ||
116 | statusCodeExpected: 400 | ||
117 | }) | ||
118 | }) | ||
119 | |||
120 | it('Should fail with a non authenticated user', async function () { | ||
121 | await makePostBodyRequest({ | ||
122 | url: server.url, | ||
123 | path, | ||
124 | fields: { | ||
125 | ids: [ 5 ] | ||
126 | }, | ||
127 | statusCodeExpected: 401 | ||
128 | }) | ||
129 | }) | ||
130 | |||
131 | it('Should succeed with the correct parameters', async function () { | ||
132 | await makePostBodyRequest({ | ||
133 | url: server.url, | ||
134 | path, | ||
135 | fields: { | ||
136 | ids: [ 5 ] | ||
137 | }, | ||
138 | token: server.accessToken, | ||
139 | statusCodeExpected: 204 | ||
140 | }) | ||
141 | }) | ||
142 | }) | ||
143 | |||
144 | describe('When marking as read my notifications', function () { | ||
145 | const path = '/api/v1/users/me/notifications/read-all' | ||
146 | |||
147 | it('Should fail with a non authenticated user', async function () { | ||
148 | await makePostBodyRequest({ | ||
149 | url: server.url, | ||
150 | path, | ||
151 | statusCodeExpected: 401 | ||
152 | }) | ||
153 | }) | ||
154 | |||
155 | it('Should succeed with the correct parameters', async function () { | ||
156 | await makePostBodyRequest({ | ||
157 | url: server.url, | ||
158 | path, | ||
159 | token: server.accessToken, | ||
160 | statusCodeExpected: 204 | ||
161 | }) | ||
162 | }) | ||
163 | }) | ||
164 | |||
165 | describe('When updating my notification settings', function () { | ||
166 | const path = '/api/v1/users/me/notification-settings' | ||
167 | const correctFields: UserNotificationSetting = { | ||
168 | newVideoFromSubscription: UserNotificationSettingValue.WEB, | ||
169 | newCommentOnMyVideo: UserNotificationSettingValue.WEB, | ||
170 | videoAbuseAsModerator: UserNotificationSettingValue.WEB, | ||
171 | blacklistOnMyVideo: UserNotificationSettingValue.WEB, | ||
172 | myVideoImportFinished: UserNotificationSettingValue.WEB, | ||
173 | myVideoPublished: UserNotificationSettingValue.WEB, | ||
174 | commentMention: UserNotificationSettingValue.WEB, | ||
175 | newFollow: UserNotificationSettingValue.WEB, | ||
176 | newUserRegistration: UserNotificationSettingValue.WEB | ||
177 | } | ||
178 | |||
179 | it('Should fail with missing fields', async function () { | ||
180 | await makePutBodyRequest({ | ||
181 | url: server.url, | ||
182 | path, | ||
183 | token: server.accessToken, | ||
184 | fields: { newVideoFromSubscription: UserNotificationSettingValue.WEB }, | ||
185 | statusCodeExpected: 400 | ||
186 | }) | ||
187 | }) | ||
188 | |||
189 | it('Should fail with incorrect field values', async function () { | ||
190 | { | ||
191 | const fields = immutableAssign(correctFields, { newCommentOnMyVideo: 15 }) | ||
192 | |||
193 | await makePutBodyRequest({ | ||
194 | url: server.url, | ||
195 | path, | ||
196 | token: server.accessToken, | ||
197 | fields, | ||
198 | statusCodeExpected: 400 | ||
199 | }) | ||
200 | } | ||
201 | |||
202 | { | ||
203 | const fields = immutableAssign(correctFields, { newCommentOnMyVideo: 'toto' }) | ||
204 | |||
205 | await makePutBodyRequest({ | ||
206 | url: server.url, | ||
207 | path, | ||
208 | fields, | ||
209 | token: server.accessToken, | ||
210 | statusCodeExpected: 400 | ||
211 | }) | ||
212 | } | ||
213 | }) | ||
214 | |||
215 | it('Should fail with a non authenticated user', async function () { | ||
216 | await makePutBodyRequest({ | ||
217 | url: server.url, | ||
218 | path, | ||
219 | fields: correctFields, | ||
220 | statusCodeExpected: 401 | ||
221 | }) | ||
222 | }) | ||
223 | |||
224 | it('Should succeed with the correct parameters', async function () { | ||
225 | await makePutBodyRequest({ | ||
226 | url: server.url, | ||
227 | path, | ||
228 | token: server.accessToken, | ||
229 | fields: correctFields, | ||
230 | statusCodeExpected: 204 | ||
231 | }) | ||
232 | }) | ||
233 | }) | ||
234 | |||
235 | describe('When connecting to my notification socket', function () { | ||
236 | it('Should fail with no token', function (next) { | ||
237 | const socket = io('http://localhost:9001/user-notifications', { reconnection: false }) | ||
238 | |||
239 | socket.on('error', () => { | ||
240 | socket.removeListener('error', this) | ||
241 | socket.disconnect() | ||
242 | next() | ||
243 | }) | ||
244 | |||
245 | socket.on('connect', () => { | ||
246 | socket.disconnect() | ||
247 | next(new Error('Connected with a missing token.')) | ||
248 | }) | ||
249 | }) | ||
250 | |||
251 | it('Should fail with an invalid token', function (next) { | ||
252 | const socket = io('http://localhost:9001/user-notifications', { | ||
253 | query: { accessToken: 'bad_access_token' }, | ||
254 | reconnection: false | ||
255 | }) | ||
256 | |||
257 | socket.on('error', () => { | ||
258 | socket.removeListener('error', this) | ||
259 | socket.disconnect() | ||
260 | next() | ||
261 | }) | ||
262 | |||
263 | socket.on('connect', () => { | ||
264 | socket.disconnect() | ||
265 | next(new Error('Connected with an invalid token.')) | ||
266 | }) | ||
267 | }) | ||
268 | |||
269 | it('Should success with the correct token', function (next) { | ||
270 | const socket = io('http://localhost:9001/user-notifications', { | ||
271 | query: { accessToken: server.accessToken }, | ||
272 | reconnection: false | ||
273 | }) | ||
274 | |||
275 | const errorListener = socket.on('error', err => { | ||
276 | next(new Error('Error in connection: ' + err)) | ||
277 | }) | ||
278 | |||
279 | socket.on('connect', async () => { | ||
280 | socket.removeListener('error', errorListener) | ||
281 | socket.disconnect() | ||
282 | |||
283 | await wait(500) | ||
284 | next() | ||
285 | }) | ||
286 | }) | ||
287 | }) | ||
288 | |||
289 | after(async function () { | ||
290 | killallServers([ server ]) | ||
291 | |||
292 | // Keep the logs if the test failed | ||
293 | if (this['ok']) { | ||
294 | await flushTests() | ||
295 | } | ||
296 | }) | ||
297 | }) | ||
diff --git a/server/tests/api/check-params/user-subscriptions.ts b/server/tests/api/check-params/user-subscriptions.ts index 9fba99ac8..8a9ced7c1 100644 --- a/server/tests/api/check-params/user-subscriptions.ts +++ b/server/tests/api/check-params/user-subscriptions.ts | |||
@@ -13,8 +13,14 @@ import { | |||
13 | ServerInfo, | 13 | ServerInfo, |
14 | setAccessTokensToServers, | 14 | setAccessTokensToServers, |
15 | userLogin | 15 | userLogin |
16 | } from '../../utils' | 16 | } from '../../../../shared/utils' |
17 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 17 | |
18 | import { | ||
19 | checkBadCountPagination, | ||
20 | checkBadSortPagination, | ||
21 | checkBadStartPagination | ||
22 | } from '../../../../shared/utils/requests/check-api-params' | ||
23 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
18 | 24 | ||
19 | describe('Test user subscriptions API validators', function () { | 25 | describe('Test user subscriptions API validators', function () { |
20 | const path = '/api/v1/users/me/subscriptions' | 26 | const path = '/api/v1/users/me/subscriptions' |
@@ -141,6 +147,8 @@ describe('Test user subscriptions API validators', function () { | |||
141 | }) | 147 | }) |
142 | 148 | ||
143 | it('Should succeed with the correct parameters', async function () { | 149 | it('Should succeed with the correct parameters', async function () { |
150 | this.timeout(20000) | ||
151 | |||
144 | await makePostBodyRequest({ | 152 | await makePostBodyRequest({ |
145 | url: server.url, | 153 | url: server.url, |
146 | path, | 154 | path, |
@@ -148,6 +156,8 @@ describe('Test user subscriptions API validators', function () { | |||
148 | fields: { uri: 'user1_channel@localhost:9001' }, | 156 | fields: { uri: 'user1_channel@localhost:9001' }, |
149 | statusCodeExpected: 204 | 157 | statusCodeExpected: 204 |
150 | }) | 158 | }) |
159 | |||
160 | await waitJobs([ server ]) | ||
151 | }) | 161 | }) |
152 | }) | 162 | }) |
153 | 163 | ||
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index cbfa0c137..13be8b460 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -9,11 +9,15 @@ import { | |||
9 | createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest, | 9 | createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest, |
10 | makePostBodyRequest, makeUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, | 10 | makePostBodyRequest, makeUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, |
11 | updateUser, uploadVideo, userLogin, deleteMe, unblockUser, blockUser | 11 | updateUser, uploadVideo, userLogin, deleteMe, unblockUser, blockUser |
12 | } from '../../utils' | 12 | } from '../../../../shared/utils' |
13 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 13 | import { |
14 | import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../utils/videos/video-imports' | 14 | checkBadCountPagination, |
15 | checkBadSortPagination, | ||
16 | checkBadStartPagination | ||
17 | } from '../../../../shared/utils/requests/check-api-params' | ||
18 | import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/utils/videos/video-imports' | ||
15 | import { VideoPrivacy } from '../../../../shared/models/videos' | 19 | import { VideoPrivacy } from '../../../../shared/models/videos' |
16 | import { waitJobs } from '../../utils/server/jobs' | 20 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
17 | import { expect } from 'chai' | 21 | import { expect } from 'chai' |
18 | 22 | ||
19 | describe('Test users API validators', function () { | 23 | describe('Test users API validators', function () { |
@@ -99,13 +103,13 @@ describe('Test users API validators', function () { | |||
99 | } | 103 | } |
100 | 104 | ||
101 | it('Should fail with a too small username', async function () { | 105 | it('Should fail with a too small username', async function () { |
102 | const fields = immutableAssign(baseCorrectParams, { username: 'fi' }) | 106 | const fields = immutableAssign(baseCorrectParams, { username: '' }) |
103 | 107 | ||
104 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 108 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
105 | }) | 109 | }) |
106 | 110 | ||
107 | it('Should fail with a too long username', async function () { | 111 | it('Should fail with a too long username', async function () { |
108 | const fields = immutableAssign(baseCorrectParams, { username: 'my_super_username_which_is_very_long' }) | 112 | const fields = immutableAssign(baseCorrectParams, { username: 'super'.repeat(50) }) |
109 | 113 | ||
110 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 114 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
111 | }) | 115 | }) |
@@ -304,6 +308,14 @@ describe('Test users API validators', function () { | |||
304 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) | 308 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) |
305 | }) | 309 | }) |
306 | 310 | ||
311 | it('Should fail with an invalid videosHistoryEnabled attribute', async function () { | ||
312 | const fields = { | ||
313 | videosHistoryEnabled: -1 | ||
314 | } | ||
315 | |||
316 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) | ||
317 | }) | ||
318 | |||
307 | it('Should fail with an non authenticated user', async function () { | 319 | it('Should fail with an non authenticated user', async function () { |
308 | const fields = { | 320 | const fields = { |
309 | currentPassword: 'my super password', | 321 | currentPassword: 'my super password', |
@@ -315,7 +327,7 @@ describe('Test users API validators', function () { | |||
315 | 327 | ||
316 | it('Should fail with a too long description', async function () { | 328 | it('Should fail with a too long description', async function () { |
317 | const fields = { | 329 | const fields = { |
318 | description: 'super'.repeat(60) | 330 | description: 'super'.repeat(201) |
319 | } | 331 | } |
320 | 332 | ||
321 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) | 333 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) |
@@ -428,6 +440,14 @@ describe('Test users API validators', function () { | |||
428 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | 440 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) |
429 | }) | 441 | }) |
430 | 442 | ||
443 | it('Should fail with an invalid emailVerified attribute', async function () { | ||
444 | const fields = { | ||
445 | emailVerified: 'yes' | ||
446 | } | ||
447 | |||
448 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
449 | }) | ||
450 | |||
431 | it('Should fail with an invalid videoQuota attribute', async function () { | 451 | it('Should fail with an invalid videoQuota attribute', async function () { |
432 | const fields = { | 452 | const fields = { |
433 | videoQuota: -90 | 453 | videoQuota: -90 |
@@ -444,6 +464,24 @@ describe('Test users API validators', function () { | |||
444 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | 464 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) |
445 | }) | 465 | }) |
446 | 466 | ||
467 | it('Should fail with a too small password', async function () { | ||
468 | const fields = { | ||
469 | currentPassword: 'my super password', | ||
470 | password: 'bla' | ||
471 | } | ||
472 | |||
473 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
474 | }) | ||
475 | |||
476 | it('Should fail with a too long password', async function () { | ||
477 | const fields = { | ||
478 | currentPassword: 'my super password', | ||
479 | password: 'super'.repeat(61) | ||
480 | } | ||
481 | |||
482 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
483 | }) | ||
484 | |||
447 | it('Should fail with an non authenticated user', async function () { | 485 | it('Should fail with an non authenticated user', async function () { |
448 | const fields = { | 486 | const fields = { |
449 | videoQuota: 42 | 487 | videoQuota: 42 |
@@ -463,12 +501,12 @@ describe('Test users API validators', function () { | |||
463 | it('Should succeed with the correct params', async function () { | 501 | it('Should succeed with the correct params', async function () { |
464 | const fields = { | 502 | const fields = { |
465 | email: 'email@example.com', | 503 | email: 'email@example.com', |
504 | emailVerified: true, | ||
466 | videoQuota: 42, | 505 | videoQuota: 42, |
467 | role: UserRole.MODERATOR | 506 | role: UserRole.USER |
468 | } | 507 | } |
469 | 508 | ||
470 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields, statusCodeExpected: 204 }) | 509 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields, statusCodeExpected: 204 }) |
471 | userAccessToken = await userLogin(server, user) | ||
472 | }) | 510 | }) |
473 | }) | 511 | }) |
474 | 512 | ||
@@ -541,13 +579,13 @@ describe('Test users API validators', function () { | |||
541 | } | 579 | } |
542 | 580 | ||
543 | it('Should fail with a too small username', async function () { | 581 | it('Should fail with a too small username', async function () { |
544 | const fields = immutableAssign(baseCorrectParams, { username: 'ji' }) | 582 | const fields = immutableAssign(baseCorrectParams, { username: '' }) |
545 | 583 | ||
546 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | 584 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) |
547 | }) | 585 | }) |
548 | 586 | ||
549 | it('Should fail with a too long username', async function () { | 587 | it('Should fail with a too long username', async function () { |
550 | const fields = immutableAssign(baseCorrectParams, { username: 'my_super_username_which_is_very_long' }) | 588 | const fields = immutableAssign(baseCorrectParams, { username: 'super'.repeat(50) }) |
551 | 589 | ||
552 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | 590 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) |
553 | }) | 591 | }) |
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts index d2bed6a2a..3b8f5f14d 100644 --- a/server/tests/api/check-params/video-abuses.ts +++ b/server/tests/api/check-params/video-abuses.ts | |||
@@ -15,8 +15,12 @@ import { | |||
15 | updateVideoAbuse, | 15 | updateVideoAbuse, |
16 | uploadVideo, | 16 | uploadVideo, |
17 | userLogin | 17 | userLogin |
18 | } from '../../utils' | 18 | } from '../../../../shared/utils' |
19 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 19 | import { |
20 | checkBadCountPagination, | ||
21 | checkBadSortPagination, | ||
22 | checkBadStartPagination | ||
23 | } from '../../../../shared/utils/requests/check-api-params' | ||
20 | import { VideoAbuseState } from '../../../../shared/models/videos' | 24 | import { VideoAbuseState } from '../../../../shared/models/videos' |
21 | 25 | ||
22 | describe('Test video abuses API validators', function () { | 26 | describe('Test video abuses API validators', function () { |
@@ -109,8 +113,8 @@ describe('Test video abuses API validators', function () { | |||
109 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 113 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
110 | }) | 114 | }) |
111 | 115 | ||
112 | it('Should fail with a reason too big', async function () { | 116 | it('Should fail with a too big reason', async function () { |
113 | const fields = { reason: 'super'.repeat(61) } | 117 | const fields = { reason: 'super'.repeat(605) } |
114 | 118 | ||
115 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 119 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
116 | }) | 120 | }) |
@@ -150,7 +154,7 @@ describe('Test video abuses API validators', function () { | |||
150 | }) | 154 | }) |
151 | 155 | ||
152 | it('Should fail with a bad moderation comment', async function () { | 156 | it('Should fail with a bad moderation comment', async function () { |
153 | const body = { moderationComment: 'b'.repeat(305) } | 157 | const body = { moderationComment: 'b'.repeat(3001) } |
154 | await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400) | 158 | await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400) |
155 | }) | 159 | }) |
156 | 160 | ||
diff --git a/server/tests/api/check-params/video-blacklist.ts b/server/tests/api/check-params/video-blacklist.ts index 473216236..6b82643f4 100644 --- a/server/tests/api/check-params/video-blacklist.ts +++ b/server/tests/api/check-params/video-blacklist.ts | |||
@@ -4,25 +4,33 @@ import 'mocha' | |||
4 | 4 | ||
5 | import { | 5 | import { |
6 | createUser, | 6 | createUser, |
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
7 | flushTests, | 9 | flushTests, |
8 | getBlacklistedVideosList, getVideo, getVideoWithToken, | 10 | getBlacklistedVideosList, |
11 | getVideo, | ||
12 | getVideoWithToken, | ||
9 | killallServers, | 13 | killallServers, |
10 | makePostBodyRequest, | 14 | makePostBodyRequest, |
11 | makePutBodyRequest, | 15 | makePutBodyRequest, |
12 | removeVideoFromBlacklist, | 16 | removeVideoFromBlacklist, |
13 | runServer, | ||
14 | ServerInfo, | 17 | ServerInfo, |
15 | setAccessTokensToServers, | 18 | setAccessTokensToServers, |
16 | uploadVideo, | 19 | uploadVideo, |
17 | userLogin | 20 | userLogin, waitJobs |
18 | } from '../../utils' | 21 | } from '../../../../shared/utils' |
19 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 22 | import { |
23 | checkBadCountPagination, | ||
24 | checkBadSortPagination, | ||
25 | checkBadStartPagination | ||
26 | } from '../../../../shared/utils/requests/check-api-params' | ||
20 | import { VideoDetails } from '../../../../shared/models/videos' | 27 | import { VideoDetails } from '../../../../shared/models/videos' |
21 | import { expect } from 'chai' | 28 | import { expect } from 'chai' |
22 | 29 | ||
23 | describe('Test video blacklist API validators', function () { | 30 | describe('Test video blacklist API validators', function () { |
24 | let server: ServerInfo | 31 | let servers: ServerInfo[] |
25 | let notBlacklistedVideoId: number | 32 | let notBlacklistedVideoId: number |
33 | let remoteVideoUUID: string | ||
26 | let userAccessToken1 = '' | 34 | let userAccessToken1 = '' |
27 | let userAccessToken2 = '' | 35 | let userAccessToken2 = '' |
28 | 36 | ||
@@ -32,75 +40,89 @@ describe('Test video blacklist API validators', function () { | |||
32 | this.timeout(120000) | 40 | this.timeout(120000) |
33 | 41 | ||
34 | await flushTests() | 42 | await flushTests() |
43 | servers = await flushAndRunMultipleServers(2) | ||
35 | 44 | ||
36 | server = await runServer(1) | 45 | await setAccessTokensToServers(servers) |
37 | 46 | await doubleFollow(servers[0], servers[1]) | |
38 | await setAccessTokensToServers([ server ]) | ||
39 | 47 | ||
40 | { | 48 | { |
41 | const username = 'user1' | 49 | const username = 'user1' |
42 | const password = 'my super password' | 50 | const password = 'my super password' |
43 | await createUser(server.url, server.accessToken, username, password) | 51 | await createUser(servers[0].url, servers[0].accessToken, username, password) |
44 | userAccessToken1 = await userLogin(server, { username, password }) | 52 | userAccessToken1 = await userLogin(servers[0], { username, password }) |
45 | } | 53 | } |
46 | 54 | ||
47 | { | 55 | { |
48 | const username = 'user2' | 56 | const username = 'user2' |
49 | const password = 'my super password' | 57 | const password = 'my super password' |
50 | await createUser(server.url, server.accessToken, username, password) | 58 | await createUser(servers[0].url, servers[0].accessToken, username, password) |
51 | userAccessToken2 = await userLogin(server, { username, password }) | 59 | userAccessToken2 = await userLogin(servers[0], { username, password }) |
52 | } | 60 | } |
53 | 61 | ||
54 | { | 62 | { |
55 | const res = await uploadVideo(server.url, userAccessToken1, {}) | 63 | const res = await uploadVideo(servers[0].url, userAccessToken1, {}) |
56 | server.video = res.body.video | 64 | servers[0].video = res.body.video |
57 | } | 65 | } |
58 | 66 | ||
59 | { | 67 | { |
60 | const res = await uploadVideo(server.url, server.accessToken, {}) | 68 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, {}) |
61 | notBlacklistedVideoId = res.body.video.uuid | 69 | notBlacklistedVideoId = res.body.video.uuid |
62 | } | 70 | } |
71 | |||
72 | { | ||
73 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, {}) | ||
74 | remoteVideoUUID = res.body.video.uuid | ||
75 | } | ||
76 | |||
77 | await waitJobs(servers) | ||
63 | }) | 78 | }) |
64 | 79 | ||
65 | describe('When adding a video in blacklist', function () { | 80 | describe('When adding a video in blacklist', function () { |
66 | const basePath = '/api/v1/videos/' | 81 | const basePath = '/api/v1/videos/' |
67 | 82 | ||
68 | it('Should fail with nothing', async function () { | 83 | it('Should fail with nothing', async function () { |
69 | const path = basePath + server.video + '/blacklist' | 84 | const path = basePath + servers[0].video + '/blacklist' |
70 | const fields = {} | 85 | const fields = {} |
71 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 86 | await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) |
72 | }) | 87 | }) |
73 | 88 | ||
74 | it('Should fail with a wrong video', async function () { | 89 | it('Should fail with a wrong video', async function () { |
75 | const wrongPath = '/api/v1/videos/blabla/blacklist' | 90 | const wrongPath = '/api/v1/videos/blabla/blacklist' |
76 | const fields = {} | 91 | const fields = {} |
77 | await makePostBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields }) | 92 | await makePostBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields }) |
78 | }) | 93 | }) |
79 | 94 | ||
80 | it('Should fail with a non authenticated user', async function () { | 95 | it('Should fail with a non authenticated user', async function () { |
81 | const path = basePath + server.video + '/blacklist' | 96 | const path = basePath + servers[0].video + '/blacklist' |
82 | const fields = {} | 97 | const fields = {} |
83 | await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 }) | 98 | await makePostBodyRequest({ url: servers[0].url, path, token: 'hello', fields, statusCodeExpected: 401 }) |
84 | }) | 99 | }) |
85 | 100 | ||
86 | it('Should fail with a non admin user', async function () { | 101 | it('Should fail with a non admin user', async function () { |
87 | const path = basePath + server.video + '/blacklist' | 102 | const path = basePath + servers[0].video + '/blacklist' |
88 | const fields = {} | 103 | const fields = {} |
89 | await makePostBodyRequest({ url: server.url, path, token: userAccessToken2, fields, statusCodeExpected: 403 }) | 104 | await makePostBodyRequest({ url: servers[0].url, path, token: userAccessToken2, fields, statusCodeExpected: 403 }) |
90 | }) | 105 | }) |
91 | 106 | ||
92 | it('Should fail with an invalid reason', async function () { | 107 | it('Should fail with an invalid reason', async function () { |
93 | const path = basePath + server.video.uuid + '/blacklist' | 108 | const path = basePath + servers[0].video.uuid + '/blacklist' |
94 | const fields = { reason: 'a'.repeat(305) } | 109 | const fields = { reason: 'a'.repeat(305) } |
95 | 110 | ||
96 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 111 | await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) |
112 | }) | ||
113 | |||
114 | it('Should fail to unfederate a remote video', async function () { | ||
115 | const path = basePath + remoteVideoUUID + '/blacklist' | ||
116 | const fields = { unfederate: true } | ||
117 | |||
118 | await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields, statusCodeExpected: 409 }) | ||
97 | }) | 119 | }) |
98 | 120 | ||
99 | it('Should succeed with the correct params', async function () { | 121 | it('Should succeed with the correct params', async function () { |
100 | const path = basePath + server.video.uuid + '/blacklist' | 122 | const path = basePath + servers[0].video.uuid + '/blacklist' |
101 | const fields = { } | 123 | const fields = { } |
102 | 124 | ||
103 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 }) | 125 | await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields, statusCodeExpected: 204 }) |
104 | }) | 126 | }) |
105 | }) | 127 | }) |
106 | 128 | ||
@@ -110,61 +132,61 @@ describe('Test video blacklist API validators', function () { | |||
110 | it('Should fail with a wrong video', async function () { | 132 | it('Should fail with a wrong video', async function () { |
111 | const wrongPath = '/api/v1/videos/blabla/blacklist' | 133 | const wrongPath = '/api/v1/videos/blabla/blacklist' |
112 | const fields = {} | 134 | const fields = {} |
113 | await makePutBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields }) | 135 | await makePutBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields }) |
114 | }) | 136 | }) |
115 | 137 | ||
116 | it('Should fail with a video not blacklisted', async function () { | 138 | it('Should fail with a video not blacklisted', async function () { |
117 | const path = '/api/v1/videos/' + notBlacklistedVideoId + '/blacklist' | 139 | const path = '/api/v1/videos/' + notBlacklistedVideoId + '/blacklist' |
118 | const fields = {} | 140 | const fields = {} |
119 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 404 }) | 141 | await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields, statusCodeExpected: 404 }) |
120 | }) | 142 | }) |
121 | 143 | ||
122 | it('Should fail with a non authenticated user', async function () { | 144 | it('Should fail with a non authenticated user', async function () { |
123 | const path = basePath + server.video + '/blacklist' | 145 | const path = basePath + servers[0].video + '/blacklist' |
124 | const fields = {} | 146 | const fields = {} |
125 | await makePutBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 }) | 147 | await makePutBodyRequest({ url: servers[0].url, path, token: 'hello', fields, statusCodeExpected: 401 }) |
126 | }) | 148 | }) |
127 | 149 | ||
128 | it('Should fail with a non admin user', async function () { | 150 | it('Should fail with a non admin user', async function () { |
129 | const path = basePath + server.video + '/blacklist' | 151 | const path = basePath + servers[0].video + '/blacklist' |
130 | const fields = {} | 152 | const fields = {} |
131 | await makePutBodyRequest({ url: server.url, path, token: userAccessToken2, fields, statusCodeExpected: 403 }) | 153 | await makePutBodyRequest({ url: servers[0].url, path, token: userAccessToken2, fields, statusCodeExpected: 403 }) |
132 | }) | 154 | }) |
133 | 155 | ||
134 | it('Should fail with an invalid reason', async function () { | 156 | it('Should fail with an invalid reason', async function () { |
135 | const path = basePath + server.video.uuid + '/blacklist' | 157 | const path = basePath + servers[0].video.uuid + '/blacklist' |
136 | const fields = { reason: 'a'.repeat(305) } | 158 | const fields = { reason: 'a'.repeat(305) } |
137 | 159 | ||
138 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 160 | await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) |
139 | }) | 161 | }) |
140 | 162 | ||
141 | it('Should succeed with the correct params', async function () { | 163 | it('Should succeed with the correct params', async function () { |
142 | const path = basePath + server.video.uuid + '/blacklist' | 164 | const path = basePath + servers[0].video.uuid + '/blacklist' |
143 | const fields = { reason: 'hello' } | 165 | const fields = { reason: 'hello' } |
144 | 166 | ||
145 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 }) | 167 | await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields, statusCodeExpected: 204 }) |
146 | }) | 168 | }) |
147 | }) | 169 | }) |
148 | 170 | ||
149 | describe('When getting blacklisted video', function () { | 171 | describe('When getting blacklisted video', function () { |
150 | 172 | ||
151 | it('Should fail with a non authenticated user', async function () { | 173 | it('Should fail with a non authenticated user', async function () { |
152 | await getVideo(server.url, server.video.uuid, 401) | 174 | await getVideo(servers[0].url, servers[0].video.uuid, 401) |
153 | }) | 175 | }) |
154 | 176 | ||
155 | it('Should fail with another user', async function () { | 177 | it('Should fail with another user', async function () { |
156 | await getVideoWithToken(server.url, userAccessToken2, server.video.uuid, 403) | 178 | await getVideoWithToken(servers[0].url, userAccessToken2, servers[0].video.uuid, 403) |
157 | }) | 179 | }) |
158 | 180 | ||
159 | it('Should succeed with the owner authenticated user', async function () { | 181 | it('Should succeed with the owner authenticated user', async function () { |
160 | const res = await getVideoWithToken(server.url, userAccessToken1, server.video.uuid, 200) | 182 | const res = await getVideoWithToken(servers[0].url, userAccessToken1, servers[0].video.uuid, 200) |
161 | const video: VideoDetails = res.body | 183 | const video: VideoDetails = res.body |
162 | 184 | ||
163 | expect(video.blacklisted).to.be.true | 185 | expect(video.blacklisted).to.be.true |
164 | }) | 186 | }) |
165 | 187 | ||
166 | it('Should succeed with an admin', async function () { | 188 | it('Should succeed with an admin', async function () { |
167 | const res = await getVideoWithToken(server.url, server.accessToken, server.video.uuid, 200) | 189 | const res = await getVideoWithToken(servers[0].url, servers[0].accessToken, servers[0].video.uuid, 200) |
168 | const video: VideoDetails = res.body | 190 | const video: VideoDetails = res.body |
169 | 191 | ||
170 | expect(video.blacklisted).to.be.true | 192 | expect(video.blacklisted).to.be.true |
@@ -173,24 +195,24 @@ describe('Test video blacklist API validators', function () { | |||
173 | 195 | ||
174 | describe('When removing a video in blacklist', function () { | 196 | describe('When removing a video in blacklist', function () { |
175 | it('Should fail with a non authenticated user', async function () { | 197 | it('Should fail with a non authenticated user', async function () { |
176 | await removeVideoFromBlacklist(server.url, 'fake token', server.video.uuid, 401) | 198 | await removeVideoFromBlacklist(servers[0].url, 'fake token', servers[0].video.uuid, 401) |
177 | }) | 199 | }) |
178 | 200 | ||
179 | it('Should fail with a non admin user', async function () { | 201 | it('Should fail with a non admin user', async function () { |
180 | await removeVideoFromBlacklist(server.url, userAccessToken2, server.video.uuid, 403) | 202 | await removeVideoFromBlacklist(servers[0].url, userAccessToken2, servers[0].video.uuid, 403) |
181 | }) | 203 | }) |
182 | 204 | ||
183 | it('Should fail with an incorrect id', async function () { | 205 | it('Should fail with an incorrect id', async function () { |
184 | await removeVideoFromBlacklist(server.url, server.accessToken, 'hello', 400) | 206 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, 'hello', 400) |
185 | }) | 207 | }) |
186 | 208 | ||
187 | it('Should fail with a not blacklisted video', async function () { | 209 | it('Should fail with a not blacklisted video', async function () { |
188 | // The video was not added to the blacklist so it should fail | 210 | // The video was not added to the blacklist so it should fail |
189 | await removeVideoFromBlacklist(server.url, server.accessToken, notBlacklistedVideoId, 404) | 211 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, notBlacklistedVideoId, 404) |
190 | }) | 212 | }) |
191 | 213 | ||
192 | it('Should succeed with the correct params', async function () { | 214 | it('Should succeed with the correct params', async function () { |
193 | await removeVideoFromBlacklist(server.url, server.accessToken, server.video.uuid, 204) | 215 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, servers[0].video.uuid, 204) |
194 | }) | 216 | }) |
195 | }) | 217 | }) |
196 | 218 | ||
@@ -198,28 +220,28 @@ describe('Test video blacklist API validators', function () { | |||
198 | const basePath = '/api/v1/videos/blacklist/' | 220 | const basePath = '/api/v1/videos/blacklist/' |
199 | 221 | ||
200 | it('Should fail with a non authenticated user', async function () { | 222 | it('Should fail with a non authenticated user', async function () { |
201 | await getBlacklistedVideosList(server.url, 'fake token', 401) | 223 | await getBlacklistedVideosList(servers[0].url, 'fake token', 401) |
202 | }) | 224 | }) |
203 | 225 | ||
204 | it('Should fail with a non admin user', async function () { | 226 | it('Should fail with a non admin user', async function () { |
205 | await getBlacklistedVideosList(server.url, userAccessToken2, 403) | 227 | await getBlacklistedVideosList(servers[0].url, userAccessToken2, 403) |
206 | }) | 228 | }) |
207 | 229 | ||
208 | it('Should fail with a bad start pagination', async function () { | 230 | it('Should fail with a bad start pagination', async function () { |
209 | await checkBadStartPagination(server.url, basePath, server.accessToken) | 231 | await checkBadStartPagination(servers[0].url, basePath, servers[0].accessToken) |
210 | }) | 232 | }) |
211 | 233 | ||
212 | it('Should fail with a bad count pagination', async function () { | 234 | it('Should fail with a bad count pagination', async function () { |
213 | await checkBadCountPagination(server.url, basePath, server.accessToken) | 235 | await checkBadCountPagination(servers[0].url, basePath, servers[0].accessToken) |
214 | }) | 236 | }) |
215 | 237 | ||
216 | it('Should fail with an incorrect sort', async function () { | 238 | it('Should fail with an incorrect sort', async function () { |
217 | await checkBadSortPagination(server.url, basePath, server.accessToken) | 239 | await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken) |
218 | }) | 240 | }) |
219 | }) | 241 | }) |
220 | 242 | ||
221 | after(async function () { | 243 | after(async function () { |
222 | killallServers([ server ]) | 244 | killallServers(servers) |
223 | 245 | ||
224 | // Keep the logs if the test failed | 246 | // Keep the logs if the test failed |
225 | if (this['ok']) { | 247 | if (this['ok']) { |
diff --git a/server/tests/api/check-params/video-captions.ts b/server/tests/api/check-params/video-captions.ts index 8d46971a1..e4d36fd4f 100644 --- a/server/tests/api/check-params/video-captions.ts +++ b/server/tests/api/check-params/video-captions.ts | |||
@@ -13,9 +13,9 @@ import { | |||
13 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | uploadVideo, | 14 | uploadVideo, |
15 | userLogin | 15 | userLogin |
16 | } from '../../utils' | 16 | } from '../../../../shared/utils' |
17 | import { join } from 'path' | 17 | import { join } from 'path' |
18 | import { createVideoCaption } from '../../utils/videos/video-captions' | 18 | import { createVideoCaption } from '../../../../shared/utils/videos/video-captions' |
19 | 19 | ||
20 | describe('Test video captions API validator', function () { | 20 | describe('Test video captions API validator', function () { |
21 | const path = '/api/v1/videos/' | 21 | const path = '/api/v1/videos/' |
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index 3a7942945..14e4deaf7 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts | |||
@@ -20,8 +20,12 @@ import { | |||
20 | ServerInfo, | 20 | ServerInfo, |
21 | setAccessTokensToServers, | 21 | setAccessTokensToServers, |
22 | userLogin | 22 | userLogin |
23 | } from '../../utils' | 23 | } from '../../../../shared/utils' |
24 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 24 | import { |
25 | checkBadCountPagination, | ||
26 | checkBadSortPagination, | ||
27 | checkBadStartPagination | ||
28 | } from '../../../../shared/utils/requests/check-api-params' | ||
25 | import { User } from '../../../../shared/models/users' | 29 | import { User } from '../../../../shared/models/users' |
26 | import { join } from 'path' | 30 | import { join } from 'path' |
27 | 31 | ||
@@ -118,12 +122,12 @@ describe('Test video channels API validator', function () { | |||
118 | }) | 122 | }) |
119 | 123 | ||
120 | it('Should fail with a long description', async function () { | 124 | it('Should fail with a long description', async function () { |
121 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(150) }) | 125 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(201) }) |
122 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | 126 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) |
123 | }) | 127 | }) |
124 | 128 | ||
125 | it('Should fail with a long support text', async function () { | 129 | it('Should fail with a long support text', async function () { |
126 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) | 130 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
127 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | 131 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) |
128 | }) | 132 | }) |
129 | 133 | ||
@@ -185,12 +189,12 @@ describe('Test video channels API validator', function () { | |||
185 | }) | 189 | }) |
186 | 190 | ||
187 | it('Should fail with a long description', async function () { | 191 | it('Should fail with a long description', async function () { |
188 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(150) }) | 192 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(201) }) |
189 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 193 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
190 | }) | 194 | }) |
191 | 195 | ||
192 | it('Should fail with a long support text', async function () { | 196 | it('Should fail with a long support text', async function () { |
193 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) | 197 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
194 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 198 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
195 | }) | 199 | }) |
196 | 200 | ||
diff --git a/server/tests/api/check-params/video-comments.ts b/server/tests/api/check-params/video-comments.ts index 5241832fe..5981780ed 100644 --- a/server/tests/api/check-params/video-comments.ts +++ b/server/tests/api/check-params/video-comments.ts | |||
@@ -6,9 +6,13 @@ import { | |||
6 | createUser, | 6 | createUser, |
7 | flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, | 7 | flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, |
8 | uploadVideo, userLogin | 8 | uploadVideo, userLogin |
9 | } from '../../utils' | 9 | } from '../../../../shared/utils' |
10 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 10 | import { |
11 | import { addVideoCommentThread } from '../../utils/videos/video-comments' | 11 | checkBadCountPagination, |
12 | checkBadSortPagination, | ||
13 | checkBadStartPagination | ||
14 | } from '../../../../shared/utils/requests/check-api-params' | ||
15 | import { addVideoCommentThread } from '../../../../shared/utils/videos/video-comments' | ||
12 | 16 | ||
13 | const expect = chai.expect | 17 | const expect = chai.expect |
14 | 18 | ||
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts index 1ffb81a38..6dd9f15f7 100644 --- a/server/tests/api/check-params/video-imports.ts +++ b/server/tests/api/check-params/video-imports.ts | |||
@@ -18,9 +18,13 @@ import { | |||
18 | setAccessTokensToServers, | 18 | setAccessTokensToServers, |
19 | updateCustomSubConfig, | 19 | updateCustomSubConfig, |
20 | userLogin | 20 | userLogin |
21 | } from '../../utils' | 21 | } from '../../../../shared/utils' |
22 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 22 | import { |
23 | import { getMagnetURI, getYoutubeVideoUrl } from '../../utils/videos/video-imports' | 23 | checkBadCountPagination, |
24 | checkBadSortPagination, | ||
25 | checkBadStartPagination | ||
26 | } from '../../../../shared/utils/requests/check-api-params' | ||
27 | import { getMagnetURI, getYoutubeVideoUrl } from '../../../../shared/utils/videos/video-imports' | ||
24 | 28 | ||
25 | describe('Test video imports API validator', function () { | 29 | describe('Test video imports API validator', function () { |
26 | const path = '/api/v1/videos/imports' | 30 | const path = '/api/v1/videos/imports' |
@@ -141,7 +145,7 @@ describe('Test video imports API validator', function () { | |||
141 | }) | 145 | }) |
142 | 146 | ||
143 | it('Should fail with a long support text', async function () { | 147 | it('Should fail with a long support text', async function () { |
144 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) | 148 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
145 | 149 | ||
146 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 150 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
147 | }) | 151 | }) |
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts new file mode 100644 index 000000000..e998c8a3d --- /dev/null +++ b/server/tests/api/check-params/videos-filter.ts | |||
@@ -0,0 +1,127 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | createUser, | ||
7 | flushTests, | ||
8 | killallServers, | ||
9 | makeGetRequest, | ||
10 | runServer, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers, | ||
13 | userLogin | ||
14 | } from '../../../../shared/utils' | ||
15 | import { UserRole } from '../../../../shared/models/users' | ||
16 | |||
17 | const expect = chai.expect | ||
18 | |||
19 | async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) { | ||
20 | const paths = [ | ||
21 | '/api/v1/video-channels/root_channel/videos', | ||
22 | '/api/v1/accounts/root/videos', | ||
23 | '/api/v1/videos', | ||
24 | '/api/v1/search/videos' | ||
25 | ] | ||
26 | |||
27 | for (const path of paths) { | ||
28 | await makeGetRequest({ | ||
29 | url: server.url, | ||
30 | path, | ||
31 | token, | ||
32 | query: { | ||
33 | filter | ||
34 | }, | ||
35 | statusCodeExpected | ||
36 | }) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | describe('Test videos filters', function () { | ||
41 | let server: ServerInfo | ||
42 | let userAccessToken: string | ||
43 | let moderatorAccessToken: string | ||
44 | |||
45 | // --------------------------------------------------------------- | ||
46 | |||
47 | before(async function () { | ||
48 | this.timeout(30000) | ||
49 | |||
50 | await flushTests() | ||
51 | |||
52 | server = await runServer(1) | ||
53 | |||
54 | await setAccessTokensToServers([ server ]) | ||
55 | |||
56 | const user = { username: 'user1', password: 'my super password' } | ||
57 | await createUser(server.url, server.accessToken, user.username, user.password) | ||
58 | userAccessToken = await userLogin(server, user) | ||
59 | |||
60 | const moderator = { username: 'moderator', password: 'my super password' } | ||
61 | await createUser( | ||
62 | server.url, | ||
63 | server.accessToken, | ||
64 | moderator.username, | ||
65 | moderator.password, | ||
66 | undefined, | ||
67 | undefined, | ||
68 | UserRole.MODERATOR | ||
69 | ) | ||
70 | moderatorAccessToken = await userLogin(server, moderator) | ||
71 | }) | ||
72 | |||
73 | describe('When setting a video filter', function () { | ||
74 | |||
75 | it('Should fail with a bad filter', async function () { | ||
76 | await testEndpoints(server, server.accessToken, 'bad-filter', 400) | ||
77 | }) | ||
78 | |||
79 | it('Should succeed with a good filter', async function () { | ||
80 | await testEndpoints(server, server.accessToken,'local', 200) | ||
81 | }) | ||
82 | |||
83 | it('Should fail to list all-local with a simple user', async function () { | ||
84 | await testEndpoints(server, userAccessToken, 'all-local', 401) | ||
85 | }) | ||
86 | |||
87 | it('Should succeed to list all-local with a moderator', async function () { | ||
88 | await testEndpoints(server, moderatorAccessToken, 'all-local', 200) | ||
89 | }) | ||
90 | |||
91 | it('Should succeed to list all-local with an admin', async function () { | ||
92 | await testEndpoints(server, server.accessToken, 'all-local', 200) | ||
93 | }) | ||
94 | |||
95 | // Because we cannot authenticate the user on the RSS endpoint | ||
96 | it('Should fail on the feeds endpoint with the all-local filter', async function () { | ||
97 | await makeGetRequest({ | ||
98 | url: server.url, | ||
99 | path: '/feeds/videos.json', | ||
100 | statusCodeExpected: 401, | ||
101 | query: { | ||
102 | filter: 'all-local' | ||
103 | } | ||
104 | }) | ||
105 | }) | ||
106 | |||
107 | it('Should succed on the feeds endpoint with the local filter', async function () { | ||
108 | await makeGetRequest({ | ||
109 | url: server.url, | ||
110 | path: '/feeds/videos.json', | ||
111 | statusCodeExpected: 200, | ||
112 | query: { | ||
113 | filter: 'local' | ||
114 | } | ||
115 | }) | ||
116 | }) | ||
117 | }) | ||
118 | |||
119 | after(async function () { | ||
120 | killallServers([ server ]) | ||
121 | |||
122 | // Keep the logs if the test failed | ||
123 | if (this['ok']) { | ||
124 | await flushTests() | ||
125 | } | ||
126 | }) | ||
127 | }) | ||
diff --git a/server/tests/api/check-params/videos-history.ts b/server/tests/api/check-params/videos-history.ts index 808c3b616..8c079a956 100644 --- a/server/tests/api/check-params/videos-history.ts +++ b/server/tests/api/check-params/videos-history.ts | |||
@@ -3,20 +3,25 @@ | |||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { | 5 | import { |
6 | checkBadCountPagination, | ||
7 | checkBadStartPagination, | ||
6 | flushTests, | 8 | flushTests, |
7 | killallServers, | 9 | killallServers, |
10 | makeGetRequest, | ||
8 | makePostBodyRequest, | 11 | makePostBodyRequest, |
9 | makePutBodyRequest, | 12 | makePutBodyRequest, |
10 | runServer, | 13 | runServer, |
11 | ServerInfo, | 14 | ServerInfo, |
12 | setAccessTokensToServers, | 15 | setAccessTokensToServers, |
13 | uploadVideo | 16 | uploadVideo |
14 | } from '../../utils' | 17 | } from '../../../../shared/utils' |
15 | 18 | ||
16 | const expect = chai.expect | 19 | const expect = chai.expect |
17 | 20 | ||
18 | describe('Test videos history API validator', function () { | 21 | describe('Test videos history API validator', function () { |
19 | let path: string | 22 | let watchingPath: string |
23 | let myHistoryPath = '/api/v1/users/me/history/videos' | ||
24 | let myHistoryRemove = myHistoryPath + '/remove' | ||
20 | let server: ServerInfo | 25 | let server: ServerInfo |
21 | 26 | ||
22 | // --------------------------------------------------------------- | 27 | // --------------------------------------------------------------- |
@@ -33,14 +38,14 @@ describe('Test videos history API validator', function () { | |||
33 | const res = await uploadVideo(server.url, server.accessToken, {}) | 38 | const res = await uploadVideo(server.url, server.accessToken, {}) |
34 | const videoUUID = res.body.video.uuid | 39 | const videoUUID = res.body.video.uuid |
35 | 40 | ||
36 | path = '/api/v1/videos/' + videoUUID + '/watching' | 41 | watchingPath = '/api/v1/videos/' + videoUUID + '/watching' |
37 | }) | 42 | }) |
38 | 43 | ||
39 | describe('When notifying a user is watching a video', function () { | 44 | describe('When notifying a user is watching a video', function () { |
40 | 45 | ||
41 | it('Should fail with an unauthenticated user', async function () { | 46 | it('Should fail with an unauthenticated user', async function () { |
42 | const fields = { currentTime: 5 } | 47 | const fields = { currentTime: 5 } |
43 | await makePutBodyRequest({ url: server.url, path, fields, statusCodeExpected: 401 }) | 48 | await makePutBodyRequest({ url: server.url, path: watchingPath, fields, statusCodeExpected: 401 }) |
44 | }) | 49 | }) |
45 | 50 | ||
46 | it('Should fail with an incorrect video id', async function () { | 51 | it('Should fail with an incorrect video id', async function () { |
@@ -58,13 +63,68 @@ describe('Test videos history API validator', function () { | |||
58 | 63 | ||
59 | it('Should fail with a bad current time', async function () { | 64 | it('Should fail with a bad current time', async function () { |
60 | const fields = { currentTime: 'hello' } | 65 | const fields = { currentTime: 'hello' } |
61 | await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 }) | 66 | await makePutBodyRequest({ url: server.url, path: watchingPath, fields, token: server.accessToken, statusCodeExpected: 400 }) |
62 | }) | 67 | }) |
63 | 68 | ||
64 | it('Should succeed with the correct parameters', async function () { | 69 | it('Should succeed with the correct parameters', async function () { |
65 | const fields = { currentTime: 5 } | 70 | const fields = { currentTime: 5 } |
66 | 71 | ||
67 | await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 204 }) | 72 | await makePutBodyRequest({ url: server.url, path: watchingPath, fields, token: server.accessToken, statusCodeExpected: 204 }) |
73 | }) | ||
74 | }) | ||
75 | |||
76 | describe('When listing user videos history', function () { | ||
77 | it('Should fail with a bad start pagination', async function () { | ||
78 | await checkBadStartPagination(server.url, myHistoryPath, server.accessToken) | ||
79 | }) | ||
80 | |||
81 | it('Should fail with a bad count pagination', async function () { | ||
82 | await checkBadCountPagination(server.url, myHistoryPath, server.accessToken) | ||
83 | }) | ||
84 | |||
85 | it('Should fail with an unauthenticated user', async function () { | ||
86 | await makeGetRequest({ url: server.url, path: myHistoryPath, statusCodeExpected: 401 }) | ||
87 | }) | ||
88 | |||
89 | it('Should succeed with the correct params', async function () { | ||
90 | await makeGetRequest({ url: server.url, token: server.accessToken, path: myHistoryPath, statusCodeExpected: 200 }) | ||
91 | }) | ||
92 | }) | ||
93 | |||
94 | describe('When removing user videos history', function () { | ||
95 | it('Should fail with an unauthenticated user', async function () { | ||
96 | await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', statusCodeExpected: 401 }) | ||
97 | }) | ||
98 | |||
99 | it('Should fail with a bad beforeDate parameter', async function () { | ||
100 | const body = { beforeDate: '15' } | ||
101 | await makePostBodyRequest({ | ||
102 | url: server.url, | ||
103 | token: server.accessToken, | ||
104 | path: myHistoryRemove, | ||
105 | fields: body, | ||
106 | statusCodeExpected: 400 | ||
107 | }) | ||
108 | }) | ||
109 | |||
110 | it('Should succeed with a valid beforeDate param', async function () { | ||
111 | const body = { beforeDate: new Date().toISOString() } | ||
112 | await makePostBodyRequest({ | ||
113 | url: server.url, | ||
114 | token: server.accessToken, | ||
115 | path: myHistoryRemove, | ||
116 | fields: body, | ||
117 | statusCodeExpected: 204 | ||
118 | }) | ||
119 | }) | ||
120 | |||
121 | it('Should succeed without body', async function () { | ||
122 | await makePostBodyRequest({ | ||
123 | url: server.url, | ||
124 | token: server.accessToken, | ||
125 | path: myHistoryRemove, | ||
126 | statusCodeExpected: 204 | ||
127 | }) | ||
68 | }) | 128 | }) |
69 | }) | 129 | }) |
70 | 130 | ||
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index bc28e2422..878ffe025 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -8,9 +8,13 @@ import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enu | |||
8 | import { | 8 | import { |
9 | createUser, flushTests, getMyUserInformation, getVideo, getVideosList, immutableAssign, killallServers, makeDeleteRequest, | 9 | createUser, flushTests, getMyUserInformation, getVideo, getVideosList, immutableAssign, killallServers, makeDeleteRequest, |
10 | makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, runServer, ServerInfo, setAccessTokensToServers, userLogin | 10 | makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, runServer, ServerInfo, setAccessTokensToServers, userLogin |
11 | } from '../../utils' | 11 | } from '../../../../shared/utils' |
12 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 12 | import { |
13 | import { getAccountsList } from '../../utils/users/accounts' | 13 | checkBadCountPagination, |
14 | checkBadSortPagination, | ||
15 | checkBadStartPagination | ||
16 | } from '../../../../shared/utils/requests/check-api-params' | ||
17 | import { getAccountsList } from '../../../../shared/utils/users/accounts' | ||
14 | 18 | ||
15 | const expect = chai.expect | 19 | const expect = chai.expect |
16 | 20 | ||
@@ -234,7 +238,7 @@ describe('Test videos API validator', function () { | |||
234 | }) | 238 | }) |
235 | 239 | ||
236 | it('Should fail with a long support text', async function () { | 240 | it('Should fail with a long support text', async function () { |
237 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) | 241 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
238 | const attaches = baseCorrectAttaches | 242 | const attaches = baseCorrectAttaches |
239 | 243 | ||
240 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 244 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) |
@@ -317,10 +321,15 @@ describe('Test videos API validator', function () { | |||
317 | 321 | ||
318 | it('Should fail without an incorrect input file', async function () { | 322 | it('Should fail without an incorrect input file', async function () { |
319 | const fields = baseCorrectParams | 323 | const fields = baseCorrectParams |
320 | const attaches = { | 324 | let attaches = { |
321 | 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short_fake.webm') | 325 | 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short_fake.webm') |
322 | } | 326 | } |
323 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 327 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) |
328 | |||
329 | attaches = { | ||
330 | 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short.mkv') | ||
331 | } | ||
332 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | ||
324 | }) | 333 | }) |
325 | 334 | ||
326 | it('Should fail with an incorrect thumbnail file', async function () { | 335 | it('Should fail with an incorrect thumbnail file', async function () { |
@@ -484,7 +493,7 @@ describe('Test videos API validator', function () { | |||
484 | }) | 493 | }) |
485 | 494 | ||
486 | it('Should fail with a long support text', async function () { | 495 | it('Should fail with a long support text', async function () { |
487 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) | 496 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
488 | 497 | ||
489 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 498 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) |
490 | }) | 499 | }) |
diff --git a/server/tests/api/index-4.ts b/server/tests/api/index-4.ts new file mode 100644 index 000000000..7d8be2b3d --- /dev/null +++ b/server/tests/api/index-4.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | import './redundancy' | ||
2 | import './activitypub' | ||
diff --git a/server/tests/api/index.ts b/server/tests/api/index.ts index 2d996dbf9..bc140f860 100644 --- a/server/tests/api/index.ts +++ b/server/tests/api/index.ts | |||
@@ -2,3 +2,4 @@ | |||
2 | import './index-1' | 2 | import './index-1' |
3 | import './index-2' | 3 | import './index-2' |
4 | import './index-3' | 4 | import './index-3' |
5 | import './index-4' | ||
diff --git a/server/tests/api/redundancy/index.ts b/server/tests/api/redundancy/index.ts new file mode 100644 index 000000000..8e69b95a6 --- /dev/null +++ b/server/tests/api/redundancy/index.ts | |||
@@ -0,0 +1 @@ | |||
import './redundancy' | |||
diff --git a/server/tests/api/server/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index 1960854b6..778611fff 100644 --- a/server/tests/api/server/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -17,16 +17,17 @@ import { | |||
17 | viewVideo, | 17 | viewVideo, |
18 | wait, | 18 | wait, |
19 | waitUntilLog, | 19 | waitUntilLog, |
20 | checkVideoFilesWereRemoved, removeVideo | 20 | checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer, checkSegmentHash |
21 | } from '../../utils' | 21 | } from '../../../../shared/utils' |
22 | import { waitJobs } from '../../utils/server/jobs' | 22 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
23 | |||
23 | import * as magnetUtil from 'magnet-uri' | 24 | import * as magnetUtil from 'magnet-uri' |
24 | import { updateRedundancy } from '../../utils/server/redundancy' | 25 | import { updateRedundancy } from '../../../../shared/utils/server/redundancy' |
25 | import { ActorFollow } from '../../../../shared/models/actors' | 26 | import { ActorFollow } from '../../../../shared/models/actors' |
26 | import { readdir } from 'fs-extra' | 27 | import { readdir } from 'fs-extra' |
27 | import { join } from 'path' | 28 | import { join } from 'path' |
28 | import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy' | 29 | import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy' |
29 | import { getStats } from '../../utils/server/stats' | 30 | import { getStats } from '../../../../shared/utils/server/stats' |
30 | import { ServerStats } from '../../../../shared/models/server/server-stats.model' | 31 | import { ServerStats } from '../../../../shared/models/server/server-stats.model' |
31 | 32 | ||
32 | const expect = chai.expect | 33 | const expect = chai.expect |
@@ -47,6 +48,11 @@ function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: numbe | |||
47 | 48 | ||
48 | async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { | 49 | async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { |
49 | const config = { | 50 | const config = { |
51 | transcoding: { | ||
52 | hls: { | ||
53 | enabled: true | ||
54 | } | ||
55 | }, | ||
50 | redundancy: { | 56 | redundancy: { |
51 | videos: { | 57 | videos: { |
52 | check_interval: '5 seconds', | 58 | check_interval: '5 seconds', |
@@ -54,7 +60,7 @@ async function runServers (strategy: VideoRedundancyStrategy, additionalParams: | |||
54 | immutableAssign({ | 60 | immutableAssign({ |
55 | min_lifetime: '1 hour', | 61 | min_lifetime: '1 hour', |
56 | strategy: strategy, | 62 | strategy: strategy, |
57 | size: '100KB' | 63 | size: '200KB' |
58 | }, additionalParams) | 64 | }, additionalParams) |
59 | ] | 65 | ] |
60 | } | 66 | } |
@@ -84,7 +90,7 @@ async function runServers (strategy: VideoRedundancyStrategy, additionalParams: | |||
84 | await waitJobs(servers) | 90 | await waitJobs(servers) |
85 | } | 91 | } |
86 | 92 | ||
87 | async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: string) { | 93 | async function check1WebSeed (videoUUID?: string) { |
88 | if (!videoUUID) videoUUID = video1Server2UUID | 94 | if (!videoUUID) videoUUID = video1Server2UUID |
89 | 95 | ||
90 | const webseeds = [ | 96 | const webseeds = [ |
@@ -92,50 +98,21 @@ async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: str | |||
92 | ] | 98 | ] |
93 | 99 | ||
94 | for (const server of servers) { | 100 | for (const server of servers) { |
95 | { | 101 | // With token to avoid issues with video follow constraints |
96 | const res = await getVideo(server.url, videoUUID) | 102 | const res = await getVideoWithToken(server.url, server.accessToken, videoUUID) |
97 | 103 | ||
98 | const video: VideoDetails = res.body | 104 | const video: VideoDetails = res.body |
99 | for (const f of video.files) { | 105 | for (const f of video.files) { |
100 | checkMagnetWebseeds(f, webseeds, server) | 106 | checkMagnetWebseeds(f, webseeds, server) |
101 | } | ||
102 | } | 107 | } |
103 | } | 108 | } |
104 | } | 109 | } |
105 | 110 | ||
106 | async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { | 111 | async function check2Webseeds (videoUUID?: string) { |
107 | const res = await getStats(servers[0].url) | ||
108 | const data: ServerStats = res.body | ||
109 | |||
110 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
111 | const stat = data.videosRedundancy[0] | ||
112 | |||
113 | expect(stat.strategy).to.equal(strategy) | ||
114 | expect(stat.totalSize).to.equal(102400) | ||
115 | expect(stat.totalUsed).to.be.at.least(1).and.below(102401) | ||
116 | expect(stat.totalVideoFiles).to.equal(4) | ||
117 | expect(stat.totalVideos).to.equal(1) | ||
118 | } | ||
119 | |||
120 | async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { | ||
121 | const res = await getStats(servers[0].url) | ||
122 | const data: ServerStats = res.body | ||
123 | |||
124 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
125 | |||
126 | const stat = data.videosRedundancy[0] | ||
127 | expect(stat.strategy).to.equal(strategy) | ||
128 | expect(stat.totalSize).to.equal(102400) | ||
129 | expect(stat.totalUsed).to.equal(0) | ||
130 | expect(stat.totalVideoFiles).to.equal(0) | ||
131 | expect(stat.totalVideos).to.equal(0) | ||
132 | } | ||
133 | |||
134 | async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: string) { | ||
135 | if (!videoUUID) videoUUID = video1Server2UUID | 112 | if (!videoUUID) videoUUID = video1Server2UUID |
136 | 113 | ||
137 | const webseeds = [ | 114 | const webseeds = [ |
138 | 'http://localhost:9001/static/webseed/' + videoUUID, | 115 | 'http://localhost:9001/static/redundancy/' + videoUUID, |
139 | 'http://localhost:9002/static/webseed/' + videoUUID | 116 | 'http://localhost:9002/static/webseed/' + videoUUID |
140 | ] | 117 | ] |
141 | 118 | ||
@@ -147,20 +124,23 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st | |||
147 | for (const file of video.files) { | 124 | for (const file of video.files) { |
148 | checkMagnetWebseeds(file, webseeds, server) | 125 | checkMagnetWebseeds(file, webseeds, server) |
149 | 126 | ||
150 | // Only servers 1 and 2 have the video | 127 | await makeGetRequest({ |
151 | if (server.serverNumber !== 3) { | 128 | url: servers[0].url, |
152 | await makeGetRequest({ | 129 | statusCodeExpected: 200, |
153 | url: server.url, | 130 | path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`, |
154 | statusCodeExpected: 200, | 131 | contentType: null |
155 | path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, | 132 | }) |
156 | contentType: null | 133 | await makeGetRequest({ |
157 | }) | 134 | url: servers[1].url, |
158 | } | 135 | statusCodeExpected: 200, |
136 | path: `/static/webseed/${videoUUID}-${file.resolution.id}.mp4`, | ||
137 | contentType: null | ||
138 | }) | ||
159 | } | 139 | } |
160 | } | 140 | } |
161 | 141 | ||
162 | for (const directory of [ 'test1', 'test2' ]) { | 142 | for (const directory of [ 'test1/redundancy', 'test2/videos' ]) { |
163 | const files = await readdir(join(root(), directory, 'videos')) | 143 | const files = await readdir(join(root(), directory)) |
164 | expect(files).to.have.length.at.least(4) | 144 | expect(files).to.have.length.at.least(4) |
165 | 145 | ||
166 | for (const resolution of [ 240, 360, 480, 720 ]) { | 146 | for (const resolution of [ 240, 360, 480, 720 ]) { |
@@ -169,6 +149,85 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st | |||
169 | } | 149 | } |
170 | } | 150 | } |
171 | 151 | ||
152 | async function check0PlaylistRedundancies (videoUUID?: string) { | ||
153 | if (!videoUUID) videoUUID = video1Server2UUID | ||
154 | |||
155 | for (const server of servers) { | ||
156 | // With token to avoid issues with video follow constraints | ||
157 | const res = await getVideoWithToken(server.url, server.accessToken, videoUUID) | ||
158 | const video: VideoDetails = res.body | ||
159 | |||
160 | expect(video.streamingPlaylists).to.be.an('array') | ||
161 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
162 | expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0) | ||
163 | } | ||
164 | } | ||
165 | |||
166 | async function check1PlaylistRedundancies (videoUUID?: string) { | ||
167 | if (!videoUUID) videoUUID = video1Server2UUID | ||
168 | |||
169 | for (const server of servers) { | ||
170 | const res = await getVideo(server.url, videoUUID) | ||
171 | const video: VideoDetails = res.body | ||
172 | |||
173 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
174 | expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1) | ||
175 | |||
176 | const redundancy = video.streamingPlaylists[0].redundancies[0] | ||
177 | |||
178 | expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID) | ||
179 | } | ||
180 | |||
181 | const baseUrlPlaylist = servers[1].url + '/static/playlists/hls' | ||
182 | const baseUrlSegment = servers[0].url + '/static/redundancy/hls' | ||
183 | |||
184 | const res = await getVideo(servers[0].url, videoUUID) | ||
185 | const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0] | ||
186 | |||
187 | for (const resolution of [ 240, 360, 480, 720 ]) { | ||
188 | await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist) | ||
189 | } | ||
190 | |||
191 | for (const directory of [ 'test1/redundancy/hls', 'test2/playlists/hls' ]) { | ||
192 | const files = await readdir(join(root(), directory, videoUUID)) | ||
193 | expect(files).to.have.length.at.least(4) | ||
194 | |||
195 | for (const resolution of [ 240, 360, 480, 720 ]) { | ||
196 | const filename = `${videoUUID}-${resolution}-fragmented.mp4` | ||
197 | |||
198 | expect(files.find(f => f === filename)).to.not.be.undefined | ||
199 | } | ||
200 | } | ||
201 | } | ||
202 | |||
203 | async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { | ||
204 | const res = await getStats(servers[0].url) | ||
205 | const data: ServerStats = res.body | ||
206 | |||
207 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
208 | const stat = data.videosRedundancy[0] | ||
209 | |||
210 | expect(stat.strategy).to.equal(strategy) | ||
211 | expect(stat.totalSize).to.equal(204800) | ||
212 | expect(stat.totalUsed).to.be.at.least(1).and.below(204801) | ||
213 | expect(stat.totalVideoFiles).to.equal(4) | ||
214 | expect(stat.totalVideos).to.equal(1) | ||
215 | } | ||
216 | |||
217 | async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { | ||
218 | const res = await getStats(servers[0].url) | ||
219 | const data: ServerStats = res.body | ||
220 | |||
221 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
222 | |||
223 | const stat = data.videosRedundancy[0] | ||
224 | expect(stat.strategy).to.equal(strategy) | ||
225 | expect(stat.totalSize).to.equal(204800) | ||
226 | expect(stat.totalUsed).to.equal(0) | ||
227 | expect(stat.totalVideoFiles).to.equal(0) | ||
228 | expect(stat.totalVideos).to.equal(0) | ||
229 | } | ||
230 | |||
172 | async function enableRedundancyOnServer1 () { | 231 | async function enableRedundancyOnServer1 () { |
173 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) | 232 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) |
174 | 233 | ||
@@ -215,7 +274,8 @@ describe('Test videos redundancy', function () { | |||
215 | }) | 274 | }) |
216 | 275 | ||
217 | it('Should have 1 webseed on the first video', async function () { | 276 | it('Should have 1 webseed on the first video', async function () { |
218 | await check1WebSeed(strategy) | 277 | await check1WebSeed() |
278 | await check0PlaylistRedundancies() | ||
219 | await checkStatsWith1Webseed(strategy) | 279 | await checkStatsWith1Webseed(strategy) |
220 | }) | 280 | }) |
221 | 281 | ||
@@ -223,28 +283,30 @@ describe('Test videos redundancy', function () { | |||
223 | return enableRedundancyOnServer1() | 283 | return enableRedundancyOnServer1() |
224 | }) | 284 | }) |
225 | 285 | ||
226 | it('Should have 2 webseed on the first video', async function () { | 286 | it('Should have 2 webseeds on the first video', async function () { |
227 | this.timeout(40000) | 287 | this.timeout(80000) |
228 | 288 | ||
229 | await waitJobs(servers) | 289 | await waitJobs(servers) |
230 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 290 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
231 | await waitJobs(servers) | 291 | await waitJobs(servers) |
232 | 292 | ||
233 | await check2Webseeds(strategy) | 293 | await check2Webseeds() |
294 | await check1PlaylistRedundancies() | ||
234 | await checkStatsWith2Webseed(strategy) | 295 | await checkStatsWith2Webseed(strategy) |
235 | }) | 296 | }) |
236 | 297 | ||
237 | it('Should undo redundancy on server 1 and remove duplicated videos', async function () { | 298 | it('Should undo redundancy on server 1 and remove duplicated videos', async function () { |
238 | this.timeout(40000) | 299 | this.timeout(80000) |
239 | 300 | ||
240 | await disableRedundancyOnServer1() | 301 | await disableRedundancyOnServer1() |
241 | 302 | ||
242 | await waitJobs(servers) | 303 | await waitJobs(servers) |
243 | await wait(5000) | 304 | await wait(5000) |
244 | 305 | ||
245 | await check1WebSeed(strategy) | 306 | await check1WebSeed() |
307 | await check0PlaylistRedundancies() | ||
246 | 308 | ||
247 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | 309 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos', join('playlists', 'hls') ]) |
248 | }) | 310 | }) |
249 | 311 | ||
250 | after(function () { | 312 | after(function () { |
@@ -262,7 +324,8 @@ describe('Test videos redundancy', function () { | |||
262 | }) | 324 | }) |
263 | 325 | ||
264 | it('Should have 1 webseed on the first video', async function () { | 326 | it('Should have 1 webseed on the first video', async function () { |
265 | await check1WebSeed(strategy) | 327 | await check1WebSeed() |
328 | await check0PlaylistRedundancies() | ||
266 | await checkStatsWith1Webseed(strategy) | 329 | await checkStatsWith1Webseed(strategy) |
267 | }) | 330 | }) |
268 | 331 | ||
@@ -270,26 +333,28 @@ describe('Test videos redundancy', function () { | |||
270 | return enableRedundancyOnServer1() | 333 | return enableRedundancyOnServer1() |
271 | }) | 334 | }) |
272 | 335 | ||
273 | it('Should have 2 webseed on the first video', async function () { | 336 | it('Should have 2 webseeds on the first video', async function () { |
274 | this.timeout(40000) | 337 | this.timeout(80000) |
275 | 338 | ||
276 | await waitJobs(servers) | 339 | await waitJobs(servers) |
277 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 340 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
278 | await waitJobs(servers) | 341 | await waitJobs(servers) |
279 | 342 | ||
280 | await check2Webseeds(strategy) | 343 | await check2Webseeds() |
344 | await check1PlaylistRedundancies() | ||
281 | await checkStatsWith2Webseed(strategy) | 345 | await checkStatsWith2Webseed(strategy) |
282 | }) | 346 | }) |
283 | 347 | ||
284 | it('Should unfollow on server 1 and remove duplicated videos', async function () { | 348 | it('Should unfollow on server 1 and remove duplicated videos', async function () { |
285 | this.timeout(40000) | 349 | this.timeout(80000) |
286 | 350 | ||
287 | await unfollow(servers[0].url, servers[0].accessToken, servers[1]) | 351 | await unfollow(servers[0].url, servers[0].accessToken, servers[1]) |
288 | 352 | ||
289 | await waitJobs(servers) | 353 | await waitJobs(servers) |
290 | await wait(5000) | 354 | await wait(5000) |
291 | 355 | ||
292 | await check1WebSeed(strategy) | 356 | await check1WebSeed() |
357 | await check0PlaylistRedundancies() | ||
293 | 358 | ||
294 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | 359 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) |
295 | }) | 360 | }) |
@@ -309,7 +374,8 @@ describe('Test videos redundancy', function () { | |||
309 | }) | 374 | }) |
310 | 375 | ||
311 | it('Should have 1 webseed on the first video', async function () { | 376 | it('Should have 1 webseed on the first video', async function () { |
312 | await check1WebSeed(strategy) | 377 | await check1WebSeed() |
378 | await check0PlaylistRedundancies() | ||
313 | await checkStatsWith1Webseed(strategy) | 379 | await checkStatsWith1Webseed(strategy) |
314 | }) | 380 | }) |
315 | 381 | ||
@@ -318,18 +384,19 @@ describe('Test videos redundancy', function () { | |||
318 | }) | 384 | }) |
319 | 385 | ||
320 | it('Should still have 1 webseed on the first video', async function () { | 386 | it('Should still have 1 webseed on the first video', async function () { |
321 | this.timeout(40000) | 387 | this.timeout(80000) |
322 | 388 | ||
323 | await waitJobs(servers) | 389 | await waitJobs(servers) |
324 | await wait(15000) | 390 | await wait(15000) |
325 | await waitJobs(servers) | 391 | await waitJobs(servers) |
326 | 392 | ||
327 | await check1WebSeed(strategy) | 393 | await check1WebSeed() |
394 | await check0PlaylistRedundancies() | ||
328 | await checkStatsWith1Webseed(strategy) | 395 | await checkStatsWith1Webseed(strategy) |
329 | }) | 396 | }) |
330 | 397 | ||
331 | it('Should view 2 times the first video to have > min_views config', async function () { | 398 | it('Should view 2 times the first video to have > min_views config', async function () { |
332 | this.timeout(40000) | 399 | this.timeout(80000) |
333 | 400 | ||
334 | await viewVideo(servers[ 0 ].url, video1Server2UUID) | 401 | await viewVideo(servers[ 0 ].url, video1Server2UUID) |
335 | await viewVideo(servers[ 2 ].url, video1Server2UUID) | 402 | await viewVideo(servers[ 2 ].url, video1Server2UUID) |
@@ -338,14 +405,15 @@ describe('Test videos redundancy', function () { | |||
338 | await waitJobs(servers) | 405 | await waitJobs(servers) |
339 | }) | 406 | }) |
340 | 407 | ||
341 | it('Should have 2 webseed on the first video', async function () { | 408 | it('Should have 2 webseeds on the first video', async function () { |
342 | this.timeout(40000) | 409 | this.timeout(80000) |
343 | 410 | ||
344 | await waitJobs(servers) | 411 | await waitJobs(servers) |
345 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 412 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
346 | await waitJobs(servers) | 413 | await waitJobs(servers) |
347 | 414 | ||
348 | await check2Webseeds(strategy) | 415 | await check2Webseeds() |
416 | await check1PlaylistRedundancies() | ||
349 | await checkStatsWith2Webseed(strategy) | 417 | await checkStatsWith2Webseed(strategy) |
350 | }) | 418 | }) |
351 | 419 | ||
@@ -400,7 +468,7 @@ describe('Test videos redundancy', function () { | |||
400 | }) | 468 | }) |
401 | 469 | ||
402 | it('Should still have 2 webseeds after 10 seconds', async function () { | 470 | it('Should still have 2 webseeds after 10 seconds', async function () { |
403 | this.timeout(40000) | 471 | this.timeout(80000) |
404 | 472 | ||
405 | await wait(10000) | 473 | await wait(10000) |
406 | 474 | ||
@@ -415,11 +483,11 @@ describe('Test videos redundancy', function () { | |||
415 | }) | 483 | }) |
416 | 484 | ||
417 | it('Should stop server 1 and expire video redundancy', async function () { | 485 | it('Should stop server 1 and expire video redundancy', async function () { |
418 | this.timeout(40000) | 486 | this.timeout(80000) |
419 | 487 | ||
420 | killallServers([ servers[0] ]) | 488 | killallServers([ servers[0] ]) |
421 | 489 | ||
422 | await wait(10000) | 490 | await wait(15000) |
423 | 491 | ||
424 | await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') | 492 | await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') |
425 | }) | 493 | }) |
@@ -441,41 +509,60 @@ describe('Test videos redundancy', function () { | |||
441 | await enableRedundancyOnServer1() | 509 | await enableRedundancyOnServer1() |
442 | 510 | ||
443 | await waitJobs(servers) | 511 | await waitJobs(servers) |
444 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 512 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
445 | await waitJobs(servers) | 513 | await waitJobs(servers) |
446 | 514 | ||
447 | await check2Webseeds(strategy) | 515 | await check2Webseeds() |
516 | await check1PlaylistRedundancies() | ||
448 | await checkStatsWith2Webseed(strategy) | 517 | await checkStatsWith2Webseed(strategy) |
449 | 518 | ||
450 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) | 519 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) |
451 | video2Server2UUID = res.body.video.uuid | 520 | video2Server2UUID = res.body.video.uuid |
452 | }) | 521 | }) |
453 | 522 | ||
454 | it('Should cache video 2 webseed on the first video', async function () { | 523 | it('Should cache video 2 webseeds on the first video', async function () { |
455 | this.timeout(50000) | 524 | this.timeout(120000) |
456 | 525 | ||
457 | await waitJobs(servers) | 526 | await waitJobs(servers) |
458 | 527 | ||
459 | await wait(7000) | 528 | let checked = false |
460 | 529 | ||
461 | try { | 530 | while (checked === false) { |
462 | await check1WebSeed(strategy, video1Server2UUID) | 531 | await wait(1000) |
463 | await check2Webseeds(strategy, video2Server2UUID) | ||
464 | } catch { | ||
465 | await wait(3000) | ||
466 | 532 | ||
467 | try { | 533 | try { |
468 | await check1WebSeed(strategy, video1Server2UUID) | 534 | await check1WebSeed(video1Server2UUID) |
469 | await check2Webseeds(strategy, video2Server2UUID) | 535 | await check0PlaylistRedundancies(video1Server2UUID) |
470 | } catch { | 536 | await check2Webseeds(video2Server2UUID) |
471 | await wait(5000) | 537 | await check1PlaylistRedundancies(video2Server2UUID) |
472 | 538 | ||
473 | await check1WebSeed(strategy, video1Server2UUID) | 539 | checked = true |
474 | await check2Webseeds(strategy, video2Server2UUID) | 540 | } catch { |
541 | checked = false | ||
475 | } | 542 | } |
476 | } | 543 | } |
477 | }) | 544 | }) |
478 | 545 | ||
546 | it('Should disable strategy and remove redundancies', async function () { | ||
547 | this.timeout(80000) | ||
548 | |||
549 | await waitJobs(servers) | ||
550 | |||
551 | killallServers([ servers[ 0 ] ]) | ||
552 | await reRunServer(servers[ 0 ], { | ||
553 | redundancy: { | ||
554 | videos: { | ||
555 | check_interval: '1 second', | ||
556 | strategies: [] | ||
557 | } | ||
558 | } | ||
559 | }) | ||
560 | |||
561 | await waitJobs(servers) | ||
562 | |||
563 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ join('redundancy', 'hls') ]) | ||
564 | }) | ||
565 | |||
479 | after(function () { | 566 | after(function () { |
480 | return cleanServers() | 567 | return cleanServers() |
481 | }) | 568 | }) |
diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts index a287c5bdf..a411e973b 100644 --- a/server/tests/api/search/search-activitypub-video-channels.ts +++ b/server/tests/api/search/search-activitypub-video-channels.ts | |||
@@ -17,10 +17,10 @@ import { | |||
17 | uploadVideo, | 17 | uploadVideo, |
18 | userLogin, | 18 | userLogin, |
19 | wait | 19 | wait |
20 | } from '../../utils' | 20 | } from '../../../../shared/utils' |
21 | import { waitJobs } from '../../utils/server/jobs' | 21 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
22 | import { VideoChannel } from '../../../../shared/models/videos' | 22 | import { VideoChannel } from '../../../../shared/models/videos' |
23 | import { searchVideoChannel } from '../../utils/search/video-channels' | 23 | import { searchVideoChannel } from '../../../../shared/utils/search/video-channels' |
24 | 24 | ||
25 | const expect = chai.expect | 25 | const expect = chai.expect |
26 | 26 | ||
diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts index 28f4fac50..f881917e7 100644 --- a/server/tests/api/search/search-activitypub-videos.ts +++ b/server/tests/api/search/search-activitypub-videos.ts | |||
@@ -16,8 +16,8 @@ import { | |||
16 | uploadVideo, | 16 | uploadVideo, |
17 | wait, | 17 | wait, |
18 | searchVideo | 18 | searchVideo |
19 | } from '../../utils' | 19 | } from '../../../../shared/utils' |
20 | import { waitJobs } from '../../utils/server/jobs' | 20 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
21 | import { Video, VideoPrivacy } from '../../../../shared/models/videos' | 21 | import { Video, VideoPrivacy } from '../../../../shared/models/videos' |
22 | 22 | ||
23 | const expect = chai.expect | 23 | const expect = chai.expect |
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts index f1392ffea..50da837da 100644 --- a/server/tests/api/search/search-videos.ts +++ b/server/tests/api/search/search-videos.ts | |||
@@ -13,7 +13,7 @@ import { | |||
13 | uploadVideo, | 13 | uploadVideo, |
14 | wait, | 14 | wait, |
15 | immutableAssign | 15 | immutableAssign |
16 | } from '../../utils' | 16 | } from '../../../../shared/utils' |
17 | 17 | ||
18 | const expect = chai.expect | 18 | const expect = chai.expect |
19 | 19 | ||
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index facd1688d..0dfe6e4fe 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -4,8 +4,11 @@ import 'mocha' | |||
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { About } from '../../../../shared/models/server/about.model' | 5 | import { About } from '../../../../shared/models/server/about.model' |
6 | import { CustomConfig } from '../../../../shared/models/server/custom-config.model' | 6 | import { CustomConfig } from '../../../../shared/models/server/custom-config.model' |
7 | import { deleteCustomConfig, getAbout, killallServers, reRunServer } from '../../utils' | ||
8 | import { | 7 | import { |
8 | deleteCustomConfig, | ||
9 | getAbout, | ||
10 | killallServers, | ||
11 | reRunServer, | ||
9 | flushTests, | 12 | flushTests, |
10 | getConfig, | 13 | getConfig, |
11 | getCustomConfig, | 14 | getCustomConfig, |
@@ -13,7 +16,8 @@ import { | |||
13 | runServer, | 16 | runServer, |
14 | setAccessTokensToServers, | 17 | setAccessTokensToServers, |
15 | updateCustomConfig | 18 | updateCustomConfig |
16 | } from '../../utils/index' | 19 | } from '../../../../shared/utils' |
20 | import { ServerConfig } from '../../../../shared/models' | ||
17 | 21 | ||
18 | const expect = chai.expect | 22 | const expect = chai.expect |
19 | 23 | ||
@@ -29,23 +33,32 @@ function checkInitialConfig (data: CustomConfig) { | |||
29 | expect(data.instance.defaultNSFWPolicy).to.equal('display') | 33 | expect(data.instance.defaultNSFWPolicy).to.equal('display') |
30 | expect(data.instance.customizations.css).to.be.empty | 34 | expect(data.instance.customizations.css).to.be.empty |
31 | expect(data.instance.customizations.javascript).to.be.empty | 35 | expect(data.instance.customizations.javascript).to.be.empty |
36 | |||
32 | expect(data.services.twitter.username).to.equal('@Chocobozzz') | 37 | expect(data.services.twitter.username).to.equal('@Chocobozzz') |
33 | expect(data.services.twitter.whitelisted).to.be.false | 38 | expect(data.services.twitter.whitelisted).to.be.false |
39 | |||
34 | expect(data.cache.previews.size).to.equal(1) | 40 | expect(data.cache.previews.size).to.equal(1) |
35 | expect(data.cache.captions.size).to.equal(1) | 41 | expect(data.cache.captions.size).to.equal(1) |
42 | |||
36 | expect(data.signup.enabled).to.be.true | 43 | expect(data.signup.enabled).to.be.true |
37 | expect(data.signup.limit).to.equal(4) | 44 | expect(data.signup.limit).to.equal(4) |
38 | expect(data.signup.requiresEmailVerification).to.be.false | 45 | expect(data.signup.requiresEmailVerification).to.be.false |
46 | |||
39 | expect(data.admin.email).to.equal('admin1@example.com') | 47 | expect(data.admin.email).to.equal('admin1@example.com') |
48 | expect(data.contactForm.enabled).to.be.true | ||
49 | |||
40 | expect(data.user.videoQuota).to.equal(5242880) | 50 | expect(data.user.videoQuota).to.equal(5242880) |
41 | expect(data.user.videoQuotaDaily).to.equal(-1) | 51 | expect(data.user.videoQuotaDaily).to.equal(-1) |
42 | expect(data.transcoding.enabled).to.be.false | 52 | expect(data.transcoding.enabled).to.be.false |
53 | expect(data.transcoding.allowAdditionalExtensions).to.be.false | ||
43 | expect(data.transcoding.threads).to.equal(2) | 54 | expect(data.transcoding.threads).to.equal(2) |
44 | expect(data.transcoding.resolutions['240p']).to.be.true | 55 | expect(data.transcoding.resolutions['240p']).to.be.true |
45 | expect(data.transcoding.resolutions['360p']).to.be.true | 56 | expect(data.transcoding.resolutions['360p']).to.be.true |
46 | expect(data.transcoding.resolutions['480p']).to.be.true | 57 | expect(data.transcoding.resolutions['480p']).to.be.true |
47 | expect(data.transcoding.resolutions['720p']).to.be.true | 58 | expect(data.transcoding.resolutions['720p']).to.be.true |
48 | expect(data.transcoding.resolutions['1080p']).to.be.true | 59 | expect(data.transcoding.resolutions['1080p']).to.be.true |
60 | expect(data.transcoding.hls.enabled).to.be.true | ||
61 | |||
49 | expect(data.import.videos.http.enabled).to.be.true | 62 | expect(data.import.videos.http.enabled).to.be.true |
50 | expect(data.import.videos.torrent.enabled).to.be.true | 63 | expect(data.import.videos.torrent.enabled).to.be.true |
51 | } | 64 | } |
@@ -59,23 +72,33 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
59 | expect(data.instance.defaultNSFWPolicy).to.equal('blur') | 72 | expect(data.instance.defaultNSFWPolicy).to.equal('blur') |
60 | expect(data.instance.customizations.javascript).to.equal('alert("coucou")') | 73 | expect(data.instance.customizations.javascript).to.equal('alert("coucou")') |
61 | expect(data.instance.customizations.css).to.equal('body { background-color: red; }') | 74 | expect(data.instance.customizations.css).to.equal('body { background-color: red; }') |
75 | |||
62 | expect(data.services.twitter.username).to.equal('@Kuja') | 76 | expect(data.services.twitter.username).to.equal('@Kuja') |
63 | expect(data.services.twitter.whitelisted).to.be.true | 77 | expect(data.services.twitter.whitelisted).to.be.true |
78 | |||
64 | expect(data.cache.previews.size).to.equal(2) | 79 | expect(data.cache.previews.size).to.equal(2) |
65 | expect(data.cache.captions.size).to.equal(3) | 80 | expect(data.cache.captions.size).to.equal(3) |
81 | |||
66 | expect(data.signup.enabled).to.be.false | 82 | expect(data.signup.enabled).to.be.false |
67 | expect(data.signup.limit).to.equal(5) | 83 | expect(data.signup.limit).to.equal(5) |
68 | expect(data.signup.requiresEmailVerification).to.be.true | 84 | expect(data.signup.requiresEmailVerification).to.be.true |
85 | |||
69 | expect(data.admin.email).to.equal('superadmin1@example.com') | 86 | expect(data.admin.email).to.equal('superadmin1@example.com') |
87 | expect(data.contactForm.enabled).to.be.false | ||
88 | |||
70 | expect(data.user.videoQuota).to.equal(5242881) | 89 | expect(data.user.videoQuota).to.equal(5242881) |
71 | expect(data.user.videoQuotaDaily).to.equal(318742) | 90 | expect(data.user.videoQuotaDaily).to.equal(318742) |
91 | |||
72 | expect(data.transcoding.enabled).to.be.true | 92 | expect(data.transcoding.enabled).to.be.true |
73 | expect(data.transcoding.threads).to.equal(1) | 93 | expect(data.transcoding.threads).to.equal(1) |
94 | expect(data.transcoding.allowAdditionalExtensions).to.be.true | ||
74 | expect(data.transcoding.resolutions['240p']).to.be.false | 95 | expect(data.transcoding.resolutions['240p']).to.be.false |
75 | expect(data.transcoding.resolutions['360p']).to.be.true | 96 | expect(data.transcoding.resolutions['360p']).to.be.true |
76 | expect(data.transcoding.resolutions['480p']).to.be.true | 97 | expect(data.transcoding.resolutions['480p']).to.be.true |
77 | expect(data.transcoding.resolutions['720p']).to.be.false | 98 | expect(data.transcoding.resolutions['720p']).to.be.false |
78 | expect(data.transcoding.resolutions['1080p']).to.be.false | 99 | expect(data.transcoding.resolutions['1080p']).to.be.false |
100 | expect(data.transcoding.hls.enabled).to.be.false | ||
101 | |||
79 | expect(data.import.videos.http.enabled).to.be.false | 102 | expect(data.import.videos.http.enabled).to.be.false |
80 | expect(data.import.videos.torrent.enabled).to.be.false | 103 | expect(data.import.videos.torrent.enabled).to.be.false |
81 | } | 104 | } |
@@ -93,7 +116,7 @@ describe('Test config', function () { | |||
93 | 116 | ||
94 | it('Should have a correct config on a server with registration enabled', async function () { | 117 | it('Should have a correct config on a server with registration enabled', async function () { |
95 | const res = await getConfig(server.url) | 118 | const res = await getConfig(server.url) |
96 | const data = res.body | 119 | const data: ServerConfig = res.body |
97 | 120 | ||
98 | expect(data.signup.allowed).to.be.true | 121 | expect(data.signup.allowed).to.be.true |
99 | }) | 122 | }) |
@@ -108,11 +131,23 @@ describe('Test config', function () { | |||
108 | ]) | 131 | ]) |
109 | 132 | ||
110 | const res = await getConfig(server.url) | 133 | const res = await getConfig(server.url) |
111 | const data = res.body | 134 | const data: ServerConfig = res.body |
112 | 135 | ||
113 | expect(data.signup.allowed).to.be.false | 136 | expect(data.signup.allowed).to.be.false |
114 | }) | 137 | }) |
115 | 138 | ||
139 | it('Should have the correct video allowed extensions', async function () { | ||
140 | const res = await getConfig(server.url) | ||
141 | const data: ServerConfig = res.body | ||
142 | |||
143 | expect(data.video.file.extensions).to.have.lengthOf(3) | ||
144 | expect(data.video.file.extensions).to.contain('.mp4') | ||
145 | expect(data.video.file.extensions).to.contain('.webm') | ||
146 | expect(data.video.file.extensions).to.contain('.ogv') | ||
147 | |||
148 | expect(data.contactForm.enabled).to.be.true | ||
149 | }) | ||
150 | |||
116 | it('Should get the customized configuration', async function () { | 151 | it('Should get the customized configuration', async function () { |
117 | const res = await getCustomConfig(server.url, server.accessToken) | 152 | const res = await getCustomConfig(server.url, server.accessToken) |
118 | const data = res.body as CustomConfig | 153 | const data = res.body as CustomConfig |
@@ -156,12 +191,16 @@ describe('Test config', function () { | |||
156 | admin: { | 191 | admin: { |
157 | email: 'superadmin1@example.com' | 192 | email: 'superadmin1@example.com' |
158 | }, | 193 | }, |
194 | contactForm: { | ||
195 | enabled: false | ||
196 | }, | ||
159 | user: { | 197 | user: { |
160 | videoQuota: 5242881, | 198 | videoQuota: 5242881, |
161 | videoQuotaDaily: 318742 | 199 | videoQuotaDaily: 318742 |
162 | }, | 200 | }, |
163 | transcoding: { | 201 | transcoding: { |
164 | enabled: true, | 202 | enabled: true, |
203 | allowAdditionalExtensions: true, | ||
165 | threads: 1, | 204 | threads: 1, |
166 | resolutions: { | 205 | resolutions: { |
167 | '240p': false, | 206 | '240p': false, |
@@ -169,6 +208,9 @@ describe('Test config', function () { | |||
169 | '480p': true, | 208 | '480p': true, |
170 | '720p': false, | 209 | '720p': false, |
171 | '1080p': false | 210 | '1080p': false |
211 | }, | ||
212 | hls: { | ||
213 | enabled: false | ||
172 | } | 214 | } |
173 | }, | 215 | }, |
174 | import: { | 216 | import: { |
@@ -190,6 +232,18 @@ describe('Test config', function () { | |||
190 | checkUpdatedConfig(data) | 232 | checkUpdatedConfig(data) |
191 | }) | 233 | }) |
192 | 234 | ||
235 | it('Should have the correct updated video allowed extensions', async function () { | ||
236 | const res = await getConfig(server.url) | ||
237 | const data: ServerConfig = res.body | ||
238 | |||
239 | expect(data.video.file.extensions).to.have.length.above(3) | ||
240 | expect(data.video.file.extensions).to.contain('.mp4') | ||
241 | expect(data.video.file.extensions).to.contain('.webm') | ||
242 | expect(data.video.file.extensions).to.contain('.ogv') | ||
243 | expect(data.video.file.extensions).to.contain('.flv') | ||
244 | expect(data.video.file.extensions).to.contain('.mkv') | ||
245 | }) | ||
246 | |||
193 | it('Should have the configuration updated after a restart', async function () { | 247 | it('Should have the configuration updated after a restart', async function () { |
194 | this.timeout(10000) | 248 | this.timeout(10000) |
195 | 249 | ||
diff --git a/server/tests/api/server/contact-form.ts b/server/tests/api/server/contact-form.ts new file mode 100644 index 000000000..93221d0a3 --- /dev/null +++ b/server/tests/api/server/contact-form.ts | |||
@@ -0,0 +1,86 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, wait } from '../../../../shared/utils' | ||
6 | import { MockSmtpServer } from '../../../../shared/utils/miscs/email' | ||
7 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
8 | import { sendContactForm } from '../../../../shared/utils/server/contact-form' | ||
9 | |||
10 | const expect = chai.expect | ||
11 | |||
12 | describe('Test contact form', function () { | ||
13 | let server: ServerInfo | ||
14 | const emails: object[] = [] | ||
15 | |||
16 | before(async function () { | ||
17 | this.timeout(30000) | ||
18 | |||
19 | await MockSmtpServer.Instance.collectEmails(emails) | ||
20 | |||
21 | await flushTests() | ||
22 | |||
23 | const overrideConfig = { | ||
24 | smtp: { | ||
25 | hostname: 'localhost' | ||
26 | } | ||
27 | } | ||
28 | server = await runServer(1, overrideConfig) | ||
29 | await setAccessTokensToServers([ server ]) | ||
30 | }) | ||
31 | |||
32 | it('Should send a contact form', async function () { | ||
33 | this.timeout(10000) | ||
34 | |||
35 | await sendContactForm({ | ||
36 | url: server.url, | ||
37 | fromEmail: 'toto@example.com', | ||
38 | body: 'my super message', | ||
39 | fromName: 'Super toto' | ||
40 | }) | ||
41 | |||
42 | await waitJobs(server) | ||
43 | |||
44 | expect(emails).to.have.lengthOf(1) | ||
45 | |||
46 | const email = emails[0] | ||
47 | |||
48 | expect(email['from'][0]['address']).equal('toto@example.com') | ||
49 | expect(email['to'][0]['address']).equal('admin1@example.com') | ||
50 | expect(email['subject']).contains('Contact form') | ||
51 | expect(email['text']).contains('my super message') | ||
52 | }) | ||
53 | |||
54 | it('Should not be able to send another contact form because of the anti spam checker', async function () { | ||
55 | await sendContactForm({ | ||
56 | url: server.url, | ||
57 | fromEmail: 'toto@example.com', | ||
58 | body: 'my super message', | ||
59 | fromName: 'Super toto' | ||
60 | }) | ||
61 | |||
62 | await sendContactForm({ | ||
63 | url: server.url, | ||
64 | fromEmail: 'toto@example.com', | ||
65 | body: 'my super message', | ||
66 | fromName: 'Super toto', | ||
67 | expectedStatus: 403 | ||
68 | }) | ||
69 | }) | ||
70 | |||
71 | it('Should be able to send another contact form after a while', async function () { | ||
72 | await wait(1000) | ||
73 | |||
74 | await sendContactForm({ | ||
75 | url: server.url, | ||
76 | fromEmail: 'toto@example.com', | ||
77 | body: 'my super message', | ||
78 | fromName: 'Super toto' | ||
79 | }) | ||
80 | }) | ||
81 | |||
82 | after(async function () { | ||
83 | MockSmtpServer.Instance.kill() | ||
84 | killallServers([ server ]) | ||
85 | }) | ||
86 | }) | ||
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts index 713a27143..f96c57b66 100644 --- a/server/tests/api/server/email.ts +++ b/server/tests/api/server/email.ts | |||
@@ -14,11 +14,14 @@ import { | |||
14 | unblockUser, | 14 | unblockUser, |
15 | uploadVideo, | 15 | uploadVideo, |
16 | userLogin, | 16 | userLogin, |
17 | verifyEmail | 17 | verifyEmail, |
18 | } from '../../utils' | 18 | flushTests, |
19 | import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' | 19 | killallServers, |
20 | import { mockSmtpServer } from '../../utils/miscs/email' | 20 | ServerInfo, |
21 | import { waitJobs } from '../../utils/server/jobs' | 21 | setAccessTokensToServers |
22 | } from '../../../../shared/utils' | ||
23 | import { MockSmtpServer } from '../../../../shared/utils/miscs/email' | ||
24 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
22 | 25 | ||
23 | const expect = chai.expect | 26 | const expect = chai.expect |
24 | 27 | ||
@@ -38,7 +41,7 @@ describe('Test emails', function () { | |||
38 | before(async function () { | 41 | before(async function () { |
39 | this.timeout(30000) | 42 | this.timeout(30000) |
40 | 43 | ||
41 | await mockSmtpServer(emails) | 44 | await MockSmtpServer.Instance.collectEmails(emails) |
42 | 45 | ||
43 | await flushTests() | 46 | await flushTests() |
44 | 47 | ||
@@ -248,6 +251,7 @@ describe('Test emails', function () { | |||
248 | }) | 251 | }) |
249 | 252 | ||
250 | after(async function () { | 253 | after(async function () { |
254 | MockSmtpServer.Instance.kill() | ||
251 | killallServers([ server ]) | 255 | killallServers([ server ]) |
252 | }) | 256 | }) |
253 | }) | 257 | }) |
diff --git a/server/tests/api/server/follow-constraints.ts b/server/tests/api/server/follow-constraints.ts new file mode 100644 index 000000000..8bb073c41 --- /dev/null +++ b/server/tests/api/server/follow-constraints.ts | |||
@@ -0,0 +1,225 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | doubleFollow, | ||
7 | getAccountVideos, | ||
8 | getVideo, | ||
9 | getVideoChannelVideos, | ||
10 | getVideoWithToken, | ||
11 | flushAndRunMultipleServers, | ||
12 | killallServers, | ||
13 | ServerInfo, | ||
14 | setAccessTokensToServers, | ||
15 | uploadVideo | ||
16 | } from '../../../../shared/utils' | ||
17 | import { unfollow } from '../../../../shared/utils/server/follows' | ||
18 | import { userLogin } from '../../../../shared/utils/users/login' | ||
19 | import { createUser } from '../../../../shared/utils/users/users' | ||
20 | |||
21 | const expect = chai.expect | ||
22 | |||
23 | describe('Test follow constraints', function () { | ||
24 | let servers: ServerInfo[] = [] | ||
25 | let video1UUID: string | ||
26 | let video2UUID: string | ||
27 | let userAccessToken: string | ||
28 | |||
29 | before(async function () { | ||
30 | this.timeout(30000) | ||
31 | |||
32 | servers = await flushAndRunMultipleServers(2) | ||
33 | |||
34 | // Get the access tokens | ||
35 | await setAccessTokensToServers(servers) | ||
36 | |||
37 | { | ||
38 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video server 1' }) | ||
39 | video1UUID = res.body.video.uuid | ||
40 | } | ||
41 | { | ||
42 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video server 2' }) | ||
43 | video2UUID = res.body.video.uuid | ||
44 | } | ||
45 | |||
46 | const user = { | ||
47 | username: 'user1', | ||
48 | password: 'super_password' | ||
49 | } | ||
50 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) | ||
51 | userAccessToken = await userLogin(servers[0], user) | ||
52 | |||
53 | await doubleFollow(servers[0], servers[1]) | ||
54 | }) | ||
55 | |||
56 | describe('With a followed instance', function () { | ||
57 | |||
58 | describe('With an unlogged user', function () { | ||
59 | |||
60 | it('Should get the local video', async function () { | ||
61 | await getVideo(servers[0].url, video1UUID, 200) | ||
62 | }) | ||
63 | |||
64 | it('Should get the remote video', async function () { | ||
65 | await getVideo(servers[0].url, video2UUID, 200) | ||
66 | }) | ||
67 | |||
68 | it('Should list local account videos', async function () { | ||
69 | const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5) | ||
70 | |||
71 | expect(res.body.total).to.equal(1) | ||
72 | expect(res.body.data).to.have.lengthOf(1) | ||
73 | }) | ||
74 | |||
75 | it('Should list remote account videos', async function () { | ||
76 | const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5) | ||
77 | |||
78 | expect(res.body.total).to.equal(1) | ||
79 | expect(res.body.data).to.have.lengthOf(1) | ||
80 | }) | ||
81 | |||
82 | it('Should list local channel videos', async function () { | ||
83 | const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5) | ||
84 | |||
85 | expect(res.body.total).to.equal(1) | ||
86 | expect(res.body.data).to.have.lengthOf(1) | ||
87 | }) | ||
88 | |||
89 | it('Should list remote channel videos', async function () { | ||
90 | const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5) | ||
91 | |||
92 | expect(res.body.total).to.equal(1) | ||
93 | expect(res.body.data).to.have.lengthOf(1) | ||
94 | }) | ||
95 | }) | ||
96 | |||
97 | describe('With a logged user', function () { | ||
98 | it('Should get the local video', async function () { | ||
99 | await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, 200) | ||
100 | }) | ||
101 | |||
102 | it('Should get the remote video', async function () { | ||
103 | await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, 200) | ||
104 | }) | ||
105 | |||
106 | it('Should list local account videos', async function () { | ||
107 | const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5) | ||
108 | |||
109 | expect(res.body.total).to.equal(1) | ||
110 | expect(res.body.data).to.have.lengthOf(1) | ||
111 | }) | ||
112 | |||
113 | it('Should list remote account videos', async function () { | ||
114 | const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5) | ||
115 | |||
116 | expect(res.body.total).to.equal(1) | ||
117 | expect(res.body.data).to.have.lengthOf(1) | ||
118 | }) | ||
119 | |||
120 | it('Should list local channel videos', async function () { | ||
121 | const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5) | ||
122 | |||
123 | expect(res.body.total).to.equal(1) | ||
124 | expect(res.body.data).to.have.lengthOf(1) | ||
125 | }) | ||
126 | |||
127 | it('Should list remote channel videos', async function () { | ||
128 | const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5) | ||
129 | |||
130 | expect(res.body.total).to.equal(1) | ||
131 | expect(res.body.data).to.have.lengthOf(1) | ||
132 | }) | ||
133 | }) | ||
134 | }) | ||
135 | |||
136 | describe('With a non followed instance', function () { | ||
137 | |||
138 | before(async function () { | ||
139 | this.timeout(30000) | ||
140 | |||
141 | await unfollow(servers[0].url, servers[0].accessToken, servers[1]) | ||
142 | }) | ||
143 | |||
144 | describe('With an unlogged user', function () { | ||
145 | |||
146 | it('Should get the local video', async function () { | ||
147 | await getVideo(servers[0].url, video1UUID, 200) | ||
148 | }) | ||
149 | |||
150 | it('Should not get the remote video', async function () { | ||
151 | await getVideo(servers[0].url, video2UUID, 403) | ||
152 | }) | ||
153 | |||
154 | it('Should list local account videos', async function () { | ||
155 | const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5) | ||
156 | |||
157 | expect(res.body.total).to.equal(1) | ||
158 | expect(res.body.data).to.have.lengthOf(1) | ||
159 | }) | ||
160 | |||
161 | it('Should not list remote account videos', async function () { | ||
162 | const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5) | ||
163 | |||
164 | expect(res.body.total).to.equal(0) | ||
165 | expect(res.body.data).to.have.lengthOf(0) | ||
166 | }) | ||
167 | |||
168 | it('Should list local channel videos', async function () { | ||
169 | const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5) | ||
170 | |||
171 | expect(res.body.total).to.equal(1) | ||
172 | expect(res.body.data).to.have.lengthOf(1) | ||
173 | }) | ||
174 | |||
175 | it('Should not list remote channel videos', async function () { | ||
176 | const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5) | ||
177 | |||
178 | expect(res.body.total).to.equal(0) | ||
179 | expect(res.body.data).to.have.lengthOf(0) | ||
180 | }) | ||
181 | }) | ||
182 | |||
183 | describe('With a logged user', function () { | ||
184 | it('Should get the local video', async function () { | ||
185 | await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, 200) | ||
186 | }) | ||
187 | |||
188 | it('Should get the remote video', async function () { | ||
189 | await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, 200) | ||
190 | }) | ||
191 | |||
192 | it('Should list local account videos', async function () { | ||
193 | const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5) | ||
194 | |||
195 | expect(res.body.total).to.equal(1) | ||
196 | expect(res.body.data).to.have.lengthOf(1) | ||
197 | }) | ||
198 | |||
199 | it('Should list remote account videos', async function () { | ||
200 | const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5) | ||
201 | |||
202 | expect(res.body.total).to.equal(1) | ||
203 | expect(res.body.data).to.have.lengthOf(1) | ||
204 | }) | ||
205 | |||
206 | it('Should list local channel videos', async function () { | ||
207 | const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5) | ||
208 | |||
209 | expect(res.body.total).to.equal(1) | ||
210 | expect(res.body.data).to.have.lengthOf(1) | ||
211 | }) | ||
212 | |||
213 | it('Should list remote channel videos', async function () { | ||
214 | const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5) | ||
215 | |||
216 | expect(res.body.total).to.equal(1) | ||
217 | expect(res.body.data).to.have.lengthOf(1) | ||
218 | }) | ||
219 | }) | ||
220 | }) | ||
221 | |||
222 | after(async function () { | ||
223 | killallServers(servers) | ||
224 | }) | ||
225 | }) | ||
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index e8914a9d4..ad4c87c73 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts | |||
@@ -4,7 +4,7 @@ import * as chai from 'chai' | |||
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { Video, VideoPrivacy } from '../../../../shared/models/videos' | 5 | import { Video, VideoPrivacy } from '../../../../shared/models/videos' |
6 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | 6 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' |
7 | import { completeVideoCheck } from '../../utils' | 7 | import { completeVideoCheck } from '../../../../shared/utils' |
8 | import { | 8 | import { |
9 | flushAndRunMultipleServers, | 9 | flushAndRunMultipleServers, |
10 | getVideosList, | 10 | getVideosList, |
@@ -12,21 +12,26 @@ import { | |||
12 | ServerInfo, | 12 | ServerInfo, |
13 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | uploadVideo | 14 | uploadVideo |
15 | } from '../../utils/index' | 15 | } from '../../../../shared/utils/index' |
16 | import { dateIsValid } from '../../utils/miscs/miscs' | 16 | import { dateIsValid } from '../../../../shared/utils/miscs/miscs' |
17 | import { follow, getFollowersListPaginationAndSort, getFollowingListPaginationAndSort, unfollow } from '../../utils/server/follows' | 17 | import { |
18 | import { expectAccountFollows } from '../../utils/users/accounts' | 18 | follow, |
19 | import { userLogin } from '../../utils/users/login' | 19 | getFollowersListPaginationAndSort, |
20 | import { createUser } from '../../utils/users/users' | 20 | getFollowingListPaginationAndSort, |
21 | unfollow | ||
22 | } from '../../../../shared/utils/server/follows' | ||
23 | import { expectAccountFollows } from '../../../../shared/utils/users/accounts' | ||
24 | import { userLogin } from '../../../../shared/utils/users/login' | ||
25 | import { createUser } from '../../../../shared/utils/users/users' | ||
21 | import { | 26 | import { |
22 | addVideoCommentReply, | 27 | addVideoCommentReply, |
23 | addVideoCommentThread, | 28 | addVideoCommentThread, |
24 | getVideoCommentThreads, | 29 | getVideoCommentThreads, |
25 | getVideoThreadComments | 30 | getVideoThreadComments |
26 | } from '../../utils/videos/video-comments' | 31 | } from '../../../../shared/utils/videos/video-comments' |
27 | import { rateVideo } from '../../utils/videos/videos' | 32 | import { rateVideo } from '../../../../shared/utils/videos/videos' |
28 | import { waitJobs } from '../../utils/server/jobs' | 33 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
29 | import { createVideoCaption, listVideoCaptions, testCaptionFile } from '../../utils/videos/video-captions' | 34 | import { createVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/utils/videos/video-captions' |
30 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' | 35 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' |
31 | 36 | ||
32 | const expect = chai.expect | 37 | const expect = chai.expect |
@@ -93,7 +98,26 @@ describe('Test follows', function () { | |||
93 | expect(server3Follow.state).to.equal('accepted') | 98 | expect(server3Follow.state).to.equal('accepted') |
94 | }) | 99 | }) |
95 | 100 | ||
96 | it('Should have 0 followings on server 1 and 2', async function () { | 101 | it('Should search followings on server 1', async function () { |
102 | { | ||
103 | const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt', ':9002') | ||
104 | const follows = res.body.data | ||
105 | |||
106 | expect(res.body.total).to.equal(1) | ||
107 | expect(follows.length).to.equal(1) | ||
108 | expect(follows[ 0 ].following.host).to.equal('localhost:9002') | ||
109 | } | ||
110 | |||
111 | { | ||
112 | const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt', 'bla') | ||
113 | const follows = res.body.data | ||
114 | |||
115 | expect(res.body.total).to.equal(0) | ||
116 | expect(follows.length).to.equal(0) | ||
117 | } | ||
118 | }) | ||
119 | |||
120 | it('Should have 0 followings on server 2 and 3', async function () { | ||
97 | for (const server of [ servers[1], servers[2] ]) { | 121 | for (const server of [ servers[1], servers[2] ]) { |
98 | const res = await getFollowingListPaginationAndSort(server.url, 0, 5, 'createdAt') | 122 | const res = await getFollowingListPaginationAndSort(server.url, 0, 5, 'createdAt') |
99 | const follows = res.body.data | 123 | const follows = res.body.data |
@@ -116,6 +140,25 @@ describe('Test follows', function () { | |||
116 | } | 140 | } |
117 | }) | 141 | }) |
118 | 142 | ||
143 | it('Should search followers on server 2', async function () { | ||
144 | { | ||
145 | const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt', '9001') | ||
146 | const follows = res.body.data | ||
147 | |||
148 | expect(res.body.total).to.equal(1) | ||
149 | expect(follows.length).to.equal(1) | ||
150 | expect(follows[ 0 ].following.host).to.equal('localhost:9003') | ||
151 | } | ||
152 | |||
153 | { | ||
154 | const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt', 'bla') | ||
155 | const follows = res.body.data | ||
156 | |||
157 | expect(res.body.total).to.equal(0) | ||
158 | expect(follows.length).to.equal(0) | ||
159 | } | ||
160 | }) | ||
161 | |||
119 | it('Should have 0 followers on server 1', async function () { | 162 | it('Should have 0 followers on server 1', async function () { |
120 | const res = await getFollowersListPaginationAndSort(servers[0].url, 0, 5, 'createdAt') | 163 | const res = await getFollowersListPaginationAndSort(servers[0].url, 0, 5, 'createdAt') |
121 | const follows = res.body.data | 164 | const follows = res.body.data |
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts index b0a3d029a..cd5acbe16 100644 --- a/server/tests/api/server/handle-down.ts +++ b/server/tests/api/server/handle-down.ts | |||
@@ -5,24 +5,30 @@ import 'mocha' | |||
5 | import { JobState, Video } from '../../../../shared/models' | 5 | import { JobState, Video } from '../../../../shared/models' |
6 | import { VideoPrivacy } from '../../../../shared/models/videos' | 6 | import { VideoPrivacy } from '../../../../shared/models/videos' |
7 | import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | 7 | import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' |
8 | import { completeVideoCheck, getVideo, immutableAssign, reRunServer, unfollow, viewVideo } from '../../utils' | 8 | |
9 | import { | 9 | import { |
10 | completeVideoCheck, | ||
10 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
12 | getVideo, | ||
11 | getVideosList, | 13 | getVideosList, |
14 | immutableAssign, | ||
12 | killallServers, | 15 | killallServers, |
16 | reRunServer, | ||
13 | ServerInfo, | 17 | ServerInfo, |
14 | setAccessTokensToServers, | 18 | setAccessTokensToServers, |
19 | unfollow, | ||
20 | updateVideo, | ||
15 | uploadVideo, | 21 | uploadVideo, |
16 | wait | 22 | wait |
17 | } from '../../utils/index' | 23 | } from '../../../../shared/utils' |
18 | import { follow, getFollowersListPaginationAndSort } from '../../utils/server/follows' | 24 | import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows' |
19 | import { getJobsListPaginationAndSort, waitJobs } from '../../utils/server/jobs' | 25 | import { getJobsListPaginationAndSort, waitJobs } from '../../../../shared/utils/server/jobs' |
20 | import { | 26 | import { |
21 | addVideoCommentReply, | 27 | addVideoCommentReply, |
22 | addVideoCommentThread, | 28 | addVideoCommentThread, |
23 | getVideoCommentThreads, | 29 | getVideoCommentThreads, |
24 | getVideoThreadComments | 30 | getVideoThreadComments |
25 | } from '../../utils/videos/video-comments' | 31 | } from '../../../../shared/utils/videos/video-comments' |
26 | 32 | ||
27 | const expect = chai.expect | 33 | const expect = chai.expect |
28 | 34 | ||
@@ -195,15 +201,15 @@ describe('Test handle downs', function () { | |||
195 | expect(res.body.data).to.have.lengthOf(2) | 201 | expect(res.body.data).to.have.lengthOf(2) |
196 | }) | 202 | }) |
197 | 203 | ||
198 | it('Should send a view to server 3, and automatically fetch the video', async function () { | 204 | it('Should send an update to server 3, and automatically fetch the video', async function () { |
199 | this.timeout(15000) | 205 | this.timeout(15000) |
200 | 206 | ||
201 | const res1 = await getVideosList(servers[2].url) | 207 | const res1 = await getVideosList(servers[2].url) |
202 | expect(res1.body.data).to.be.an('array') | 208 | expect(res1.body.data).to.be.an('array') |
203 | expect(res1.body.data).to.have.lengthOf(11) | 209 | expect(res1.body.data).to.have.lengthOf(11) |
204 | 210 | ||
205 | await viewVideo(servers[0].url, missedVideo1.uuid) | 211 | await updateVideo(servers[0].url, servers[0].accessToken, missedVideo1.uuid, { }) |
206 | await viewVideo(servers[0].url, unlistedVideo.uuid) | 212 | await updateVideo(servers[0].url, servers[0].accessToken, unlistedVideo.uuid, { }) |
207 | 213 | ||
208 | await waitJobs(servers) | 214 | await waitJobs(servers) |
209 | 215 | ||
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts index c74c68a33..1f80cc6cf 100644 --- a/server/tests/api/server/index.ts +++ b/server/tests/api/server/index.ts | |||
@@ -1,9 +1,11 @@ | |||
1 | import './config' | 1 | import './config' |
2 | import './contact-form' | ||
2 | import './email' | 3 | import './email' |
4 | import './follow-constraints' | ||
3 | import './follows' | 5 | import './follows' |
4 | import './handle-down' | 6 | import './handle-down' |
5 | import './jobs' | 7 | import './jobs' |
6 | import './redundancy' | ||
7 | import './reverse-proxy' | 8 | import './reverse-proxy' |
8 | import './stats' | 9 | import './stats' |
9 | import './tracker' | 10 | import './tracker' |
11 | import './no-client' | ||
diff --git a/server/tests/api/server/jobs.ts b/server/tests/api/server/jobs.ts index cd59d9a1b..52948b1d6 100644 --- a/server/tests/api/server/jobs.ts +++ b/server/tests/api/server/jobs.ts | |||
@@ -2,12 +2,12 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' | 5 | import { killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index' |
6 | import { doubleFollow } from '../../utils/server/follows' | 6 | import { doubleFollow } from '../../../../shared/utils/server/follows' |
7 | import { getJobsList, getJobsListPaginationAndSort, waitJobs } from '../../utils/server/jobs' | 7 | import { getJobsList, getJobsListPaginationAndSort, waitJobs } from '../../../../shared/utils/server/jobs' |
8 | import { flushAndRunMultipleServers } from '../../utils/server/servers' | 8 | import { flushAndRunMultipleServers } from '../../../../shared/utils/server/servers' |
9 | import { uploadVideo } from '../../utils/videos/videos' | 9 | import { uploadVideo } from '../../../../shared/utils/videos/videos' |
10 | import { dateIsValid } from '../../utils/miscs/miscs' | 10 | import { dateIsValid } from '../../../../shared/utils/miscs/miscs' |
11 | 11 | ||
12 | const expect = chai.expect | 12 | const expect = chai.expect |
13 | 13 | ||
diff --git a/server/tests/api/server/no-client.ts b/server/tests/api/server/no-client.ts new file mode 100644 index 000000000..3b95ce945 --- /dev/null +++ b/server/tests/api/server/no-client.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | import 'mocha' | ||
2 | import * as request from 'supertest' | ||
3 | import { | ||
4 | flushTests, | ||
5 | killallServers, | ||
6 | ServerInfo | ||
7 | } from '../../../../shared/utils' | ||
8 | import { runServer } from '../../../../shared/utils/server/servers' | ||
9 | |||
10 | describe('Start and stop server without web client routes', function () { | ||
11 | let server: ServerInfo | ||
12 | |||
13 | before(async function () { | ||
14 | this.timeout(30000) | ||
15 | |||
16 | await flushTests() | ||
17 | |||
18 | server = await runServer(1, {}, ['--no-client']) | ||
19 | }) | ||
20 | |||
21 | it('Should fail getting the client', function () { | ||
22 | const req = request(server.url) | ||
23 | .get('/') | ||
24 | |||
25 | return req.expect(404) | ||
26 | }) | ||
27 | |||
28 | after(async function () { | ||
29 | killallServers([ server ]) | ||
30 | |||
31 | // Keep the logs if the test failed | ||
32 | if (this['ok']) { | ||
33 | await flushTests() | ||
34 | } | ||
35 | }) | ||
36 | }) | ||
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts index e2c2a293e..ee0fffd5a 100644 --- a/server/tests/api/server/reverse-proxy.ts +++ b/server/tests/api/server/reverse-proxy.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | userLogin, | 15 | userLogin, |
16 | viewVideo, | 16 | viewVideo, |
17 | wait | 17 | wait |
18 | } from '../../utils' | 18 | } from '../../../../shared/utils' |
19 | const expect = chai.expect | 19 | const expect = chai.expect |
20 | 20 | ||
21 | import { | 21 | import { |
@@ -23,7 +23,7 @@ import { | |||
23 | flushTests, | 23 | flushTests, |
24 | runServer, | 24 | runServer, |
25 | registerUser, getCustomConfig, setAccessTokensToServers, updateCustomConfig | 25 | registerUser, getCustomConfig, setAccessTokensToServers, updateCustomConfig |
26 | } from '../../utils/index' | 26 | } from '../../../../shared/utils/index' |
27 | 27 | ||
28 | describe('Test application behind a reverse proxy', function () { | 28 | describe('Test application behind a reverse proxy', function () { |
29 | let server = null | 29 | let server = null |
@@ -95,7 +95,7 @@ describe('Test application behind a reverse proxy', function () { | |||
95 | it('Should rate limit logins', async function () { | 95 | it('Should rate limit logins', async function () { |
96 | const user = { username: 'root', password: 'fail' } | 96 | const user = { username: 'root', password: 'fail' } |
97 | 97 | ||
98 | for (let i = 0; i < 14; i++) { | 98 | for (let i = 0; i < 19; i++) { |
99 | await userLogin(server, user, 400) | 99 | await userLogin(server, user, 400) |
100 | } | 100 | } |
101 | 101 | ||
diff --git a/server/tests/api/server/stats.ts b/server/tests/api/server/stats.ts index cb229e876..aaa6c62f7 100644 --- a/server/tests/api/server/stats.ts +++ b/server/tests/api/server/stats.ts | |||
@@ -13,11 +13,11 @@ import { | |||
13 | uploadVideo, | 13 | uploadVideo, |
14 | viewVideo, | 14 | viewVideo, |
15 | wait | 15 | wait |
16 | } from '../../utils' | 16 | } from '../../../../shared/utils' |
17 | import { flushTests, setAccessTokensToServers } from '../../utils/index' | 17 | import { flushTests, setAccessTokensToServers } from '../../../../shared/utils/index' |
18 | import { getStats } from '../../utils/server/stats' | 18 | import { getStats } from '../../../../shared/utils/server/stats' |
19 | import { addVideoCommentThread } from '../../utils/videos/video-comments' | 19 | import { addVideoCommentThread } from '../../../../shared/utils/videos/video-comments' |
20 | import { waitJobs } from '../../utils/server/jobs' | 20 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
21 | 21 | ||
22 | const expect = chai.expect | 22 | const expect = chai.expect |
23 | 23 | ||
@@ -39,7 +39,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
39 | } | 39 | } |
40 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) | 40 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) |
41 | 41 | ||
42 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, {}) | 42 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { fixture: 'video_short.webm' }) |
43 | const videoUUID = resVideo.body.video.uuid | 43 | const videoUUID = resVideo.body.video.uuid |
44 | 44 | ||
45 | await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'comment') | 45 | await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'comment') |
@@ -60,6 +60,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
60 | expect(data.totalLocalVideoComments).to.equal(1) | 60 | expect(data.totalLocalVideoComments).to.equal(1) |
61 | expect(data.totalLocalVideos).to.equal(1) | 61 | expect(data.totalLocalVideos).to.equal(1) |
62 | expect(data.totalLocalVideoViews).to.equal(1) | 62 | expect(data.totalLocalVideoViews).to.equal(1) |
63 | expect(data.totalLocalVideoFilesSize).to.equal(218910) | ||
63 | expect(data.totalUsers).to.equal(2) | 64 | expect(data.totalUsers).to.equal(2) |
64 | expect(data.totalVideoComments).to.equal(1) | 65 | expect(data.totalVideoComments).to.equal(1) |
65 | expect(data.totalVideos).to.equal(1) | 66 | expect(data.totalVideos).to.equal(1) |
@@ -74,6 +75,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
74 | expect(data.totalLocalVideoComments).to.equal(0) | 75 | expect(data.totalLocalVideoComments).to.equal(0) |
75 | expect(data.totalLocalVideos).to.equal(0) | 76 | expect(data.totalLocalVideos).to.equal(0) |
76 | expect(data.totalLocalVideoViews).to.equal(0) | 77 | expect(data.totalLocalVideoViews).to.equal(0) |
78 | expect(data.totalLocalVideoFilesSize).to.equal(0) | ||
77 | expect(data.totalUsers).to.equal(1) | 79 | expect(data.totalUsers).to.equal(1) |
78 | expect(data.totalVideoComments).to.equal(1) | 80 | expect(data.totalVideoComments).to.equal(1) |
79 | expect(data.totalVideos).to.equal(1) | 81 | expect(data.totalVideos).to.equal(1) |
diff --git a/server/tests/api/server/tracker.ts b/server/tests/api/server/tracker.ts index 856f2f4d1..25ca00029 100644 --- a/server/tests/api/server/tracker.ts +++ b/server/tests/api/server/tracker.ts | |||
@@ -2,8 +2,8 @@ | |||
2 | 2 | ||
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { getVideo, killallServers, runServer, ServerInfo, uploadVideo } from '../../utils' | 5 | import { getVideo, killallServers, runServer, ServerInfo, uploadVideo } from '../../../../shared/utils' |
6 | import { flushTests, setAccessTokensToServers } from '../../utils/index' | 6 | import { flushTests, setAccessTokensToServers } from '../../../../shared/utils/index' |
7 | import { VideoDetails } from '../../../../shared/models/videos' | 7 | import { VideoDetails } from '../../../../shared/models/videos' |
8 | import * as WebTorrent from 'webtorrent' | 8 | import * as WebTorrent from 'webtorrent' |
9 | 9 | ||
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/users/blocklist.ts new file mode 100644 index 000000000..4bca27a94 --- /dev/null +++ b/server/tests/api/users/blocklist.ts | |||
@@ -0,0 +1,511 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { AccountBlock, ServerBlock, Video } from '../../../../shared/index' | ||
6 | import { | ||
7 | createUser, | ||
8 | doubleFollow, | ||
9 | flushAndRunMultipleServers, | ||
10 | flushTests, | ||
11 | killallServers, | ||
12 | ServerInfo, | ||
13 | uploadVideo, | ||
14 | userLogin | ||
15 | } from '../../../../shared/utils/index' | ||
16 | import { setAccessTokensToServers } from '../../../../shared/utils/users/login' | ||
17 | import { getVideosListWithToken, getVideosList } from '../../../../shared/utils/videos/videos' | ||
18 | import { | ||
19 | addVideoCommentReply, | ||
20 | addVideoCommentThread, | ||
21 | getVideoCommentThreads, | ||
22 | getVideoThreadComments | ||
23 | } from '../../../../shared/utils/videos/video-comments' | ||
24 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
25 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | ||
26 | import { | ||
27 | addAccountToAccountBlocklist, | ||
28 | addAccountToServerBlocklist, | ||
29 | addServerToAccountBlocklist, | ||
30 | addServerToServerBlocklist, | ||
31 | getAccountBlocklistByAccount, | ||
32 | getAccountBlocklistByServer, | ||
33 | getServerBlocklistByAccount, | ||
34 | getServerBlocklistByServer, | ||
35 | removeAccountFromAccountBlocklist, | ||
36 | removeAccountFromServerBlocklist, | ||
37 | removeServerFromAccountBlocklist, | ||
38 | removeServerFromServerBlocklist | ||
39 | } from '../../../../shared/utils/users/blocklist' | ||
40 | |||
41 | const expect = chai.expect | ||
42 | |||
43 | async function checkAllVideos (url: string, token: string) { | ||
44 | { | ||
45 | const res = await getVideosListWithToken(url, token) | ||
46 | |||
47 | expect(res.body.data).to.have.lengthOf(4) | ||
48 | } | ||
49 | |||
50 | { | ||
51 | const res = await getVideosList(url) | ||
52 | |||
53 | expect(res.body.data).to.have.lengthOf(4) | ||
54 | } | ||
55 | } | ||
56 | |||
57 | async function checkAllComments (url: string, token: string, videoUUID: string) { | ||
58 | const resThreads = await getVideoCommentThreads(url, videoUUID, 0, 5, '-createdAt', token) | ||
59 | |||
60 | const threads: VideoComment[] = resThreads.body.data | ||
61 | expect(threads).to.have.lengthOf(2) | ||
62 | |||
63 | for (const thread of threads) { | ||
64 | const res = await getVideoThreadComments(url, videoUUID, thread.id, token) | ||
65 | |||
66 | const tree: VideoCommentThreadTree = res.body | ||
67 | expect(tree.children).to.have.lengthOf(1) | ||
68 | } | ||
69 | } | ||
70 | |||
71 | describe('Test blocklist', function () { | ||
72 | let servers: ServerInfo[] | ||
73 | let videoUUID1: string | ||
74 | let videoUUID2: string | ||
75 | let userToken1: string | ||
76 | let userModeratorToken: string | ||
77 | let userToken2: string | ||
78 | |||
79 | before(async function () { | ||
80 | this.timeout(60000) | ||
81 | |||
82 | await flushTests() | ||
83 | |||
84 | servers = await flushAndRunMultipleServers(2) | ||
85 | await setAccessTokensToServers(servers) | ||
86 | |||
87 | { | ||
88 | const user = { username: 'user1', password: 'password' } | ||
89 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) | ||
90 | |||
91 | userToken1 = await userLogin(servers[0], user) | ||
92 | await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' }) | ||
93 | } | ||
94 | |||
95 | { | ||
96 | const user = { username: 'moderator', password: 'password' } | ||
97 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) | ||
98 | |||
99 | userModeratorToken = await userLogin(servers[0], user) | ||
100 | } | ||
101 | |||
102 | { | ||
103 | const user = { username: 'user2', password: 'password' } | ||
104 | await createUser(servers[1].url, servers[1].accessToken, user.username, user.password) | ||
105 | |||
106 | userToken2 = await userLogin(servers[1], user) | ||
107 | await uploadVideo(servers[1].url, userToken2, { name: 'video user 2' }) | ||
108 | } | ||
109 | |||
110 | { | ||
111 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video server 1' }) | ||
112 | videoUUID1 = res.body.video.uuid | ||
113 | } | ||
114 | |||
115 | { | ||
116 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video server 2' }) | ||
117 | videoUUID2 = res.body.video.uuid | ||
118 | } | ||
119 | |||
120 | await doubleFollow(servers[0], servers[1]) | ||
121 | |||
122 | { | ||
123 | const resComment = await addVideoCommentThread(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, 'comment root 1') | ||
124 | const resReply = await addVideoCommentReply(servers[ 0 ].url, userToken1, videoUUID1, resComment.body.comment.id, 'comment user 1') | ||
125 | await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resReply.body.comment.id, 'comment root 1') | ||
126 | } | ||
127 | |||
128 | { | ||
129 | const resComment = await addVideoCommentThread(servers[ 0 ].url, userToken1, videoUUID1, 'comment user 1') | ||
130 | await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resComment.body.comment.id, 'comment root 1') | ||
131 | } | ||
132 | |||
133 | await waitJobs(servers) | ||
134 | }) | ||
135 | |||
136 | describe('User blocklist', function () { | ||
137 | |||
138 | describe('When managing account blocklist', function () { | ||
139 | it('Should list all videos', function () { | ||
140 | return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
141 | }) | ||
142 | |||
143 | it('Should list the comments', function () { | ||
144 | return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) | ||
145 | }) | ||
146 | |||
147 | it('Should block a remote account', async function () { | ||
148 | await addAccountToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') | ||
149 | }) | ||
150 | |||
151 | it('Should hide its videos', async function () { | ||
152 | const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
153 | |||
154 | const videos: Video[] = res.body.data | ||
155 | expect(videos).to.have.lengthOf(3) | ||
156 | |||
157 | const v = videos.find(v => v.name === 'video user 2') | ||
158 | expect(v).to.be.undefined | ||
159 | }) | ||
160 | |||
161 | it('Should block a local account', async function () { | ||
162 | await addAccountToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') | ||
163 | }) | ||
164 | |||
165 | it('Should hide its videos', async function () { | ||
166 | const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
167 | |||
168 | const videos: Video[] = res.body.data | ||
169 | expect(videos).to.have.lengthOf(2) | ||
170 | |||
171 | const v = videos.find(v => v.name === 'video user 1') | ||
172 | expect(v).to.be.undefined | ||
173 | }) | ||
174 | |||
175 | it('Should hide its comments', async function () { | ||
176 | const resThreads = await getVideoCommentThreads(servers[ 0 ].url, videoUUID1, 0, 5, '-createdAt', servers[ 0 ].accessToken) | ||
177 | |||
178 | const threads: VideoComment[] = resThreads.body.data | ||
179 | expect(threads).to.have.lengthOf(1) | ||
180 | expect(threads[ 0 ].totalReplies).to.equal(0) | ||
181 | |||
182 | const t = threads.find(t => t.text === 'comment user 1') | ||
183 | expect(t).to.be.undefined | ||
184 | |||
185 | for (const thread of threads) { | ||
186 | const res = await getVideoThreadComments(servers[ 0 ].url, videoUUID1, thread.id, servers[ 0 ].accessToken) | ||
187 | |||
188 | const tree: VideoCommentThreadTree = res.body | ||
189 | expect(tree.children).to.have.lengthOf(0) | ||
190 | } | ||
191 | }) | ||
192 | |||
193 | it('Should list all the videos with another user', async function () { | ||
194 | return checkAllVideos(servers[ 0 ].url, userToken1) | ||
195 | }) | ||
196 | |||
197 | it('Should list all the comments with another user', async function () { | ||
198 | return checkAllComments(servers[ 0 ].url, userToken1, videoUUID1) | ||
199 | }) | ||
200 | |||
201 | it('Should list blocked accounts', async function () { | ||
202 | { | ||
203 | const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') | ||
204 | const blocks: AccountBlock[] = res.body.data | ||
205 | |||
206 | expect(res.body.total).to.equal(2) | ||
207 | |||
208 | const block = blocks[ 0 ] | ||
209 | expect(block.byAccount.displayName).to.equal('root') | ||
210 | expect(block.byAccount.name).to.equal('root') | ||
211 | expect(block.blockedAccount.displayName).to.equal('user2') | ||
212 | expect(block.blockedAccount.name).to.equal('user2') | ||
213 | expect(block.blockedAccount.host).to.equal('localhost:9002') | ||
214 | } | ||
215 | |||
216 | { | ||
217 | const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 1, 2, 'createdAt') | ||
218 | const blocks: AccountBlock[] = res.body.data | ||
219 | |||
220 | expect(res.body.total).to.equal(2) | ||
221 | |||
222 | const block = blocks[ 0 ] | ||
223 | expect(block.byAccount.displayName).to.equal('root') | ||
224 | expect(block.byAccount.name).to.equal('root') | ||
225 | expect(block.blockedAccount.displayName).to.equal('user1') | ||
226 | expect(block.blockedAccount.name).to.equal('user1') | ||
227 | expect(block.blockedAccount.host).to.equal('localhost:9001') | ||
228 | } | ||
229 | }) | ||
230 | |||
231 | it('Should unblock the remote account', async function () { | ||
232 | await removeAccountFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') | ||
233 | }) | ||
234 | |||
235 | it('Should display its videos', async function () { | ||
236 | const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
237 | |||
238 | const videos: Video[] = res.body.data | ||
239 | expect(videos).to.have.lengthOf(3) | ||
240 | |||
241 | const v = videos.find(v => v.name === 'video user 2') | ||
242 | expect(v).not.to.be.undefined | ||
243 | }) | ||
244 | |||
245 | it('Should unblock the local account', async function () { | ||
246 | await removeAccountFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') | ||
247 | }) | ||
248 | |||
249 | it('Should display its comments', function () { | ||
250 | return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) | ||
251 | }) | ||
252 | }) | ||
253 | |||
254 | describe('When managing server blocklist', function () { | ||
255 | it('Should list all videos', function () { | ||
256 | return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
257 | }) | ||
258 | |||
259 | it('Should list the comments', function () { | ||
260 | return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) | ||
261 | }) | ||
262 | |||
263 | it('Should block a remote server', async function () { | ||
264 | await addServerToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') | ||
265 | }) | ||
266 | |||
267 | it('Should hide its videos', async function () { | ||
268 | const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
269 | |||
270 | const videos: Video[] = res.body.data | ||
271 | expect(videos).to.have.lengthOf(2) | ||
272 | |||
273 | const v1 = videos.find(v => v.name === 'video user 2') | ||
274 | const v2 = videos.find(v => v.name === 'video server 2') | ||
275 | |||
276 | expect(v1).to.be.undefined | ||
277 | expect(v2).to.be.undefined | ||
278 | }) | ||
279 | |||
280 | it('Should list all the videos with another user', async function () { | ||
281 | return checkAllVideos(servers[ 0 ].url, userToken1) | ||
282 | }) | ||
283 | |||
284 | it('Should hide its comments') | ||
285 | |||
286 | it('Should list blocked servers', async function () { | ||
287 | const res = await getServerBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') | ||
288 | const blocks: ServerBlock[] = res.body.data | ||
289 | |||
290 | expect(res.body.total).to.equal(1) | ||
291 | |||
292 | const block = blocks[ 0 ] | ||
293 | expect(block.byAccount.displayName).to.equal('root') | ||
294 | expect(block.byAccount.name).to.equal('root') | ||
295 | expect(block.blockedServer.host).to.equal('localhost:9002') | ||
296 | }) | ||
297 | |||
298 | it('Should unblock the remote server', async function () { | ||
299 | await removeServerFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') | ||
300 | }) | ||
301 | |||
302 | it('Should display its videos', function () { | ||
303 | return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
304 | }) | ||
305 | |||
306 | it('Should display its comments', function () { | ||
307 | return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) | ||
308 | }) | ||
309 | }) | ||
310 | }) | ||
311 | |||
312 | describe('Server blocklist', function () { | ||
313 | |||
314 | describe('When managing account blocklist', function () { | ||
315 | it('Should list all videos', async function () { | ||
316 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
317 | await checkAllVideos(servers[ 0 ].url, token) | ||
318 | } | ||
319 | }) | ||
320 | |||
321 | it('Should list the comments', async function () { | ||
322 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
323 | await checkAllComments(servers[ 0 ].url, token, videoUUID1) | ||
324 | } | ||
325 | }) | ||
326 | |||
327 | it('Should block a remote account', async function () { | ||
328 | await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') | ||
329 | }) | ||
330 | |||
331 | it('Should hide its videos', async function () { | ||
332 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
333 | const res = await getVideosListWithToken(servers[ 0 ].url, token) | ||
334 | |||
335 | const videos: Video[] = res.body.data | ||
336 | expect(videos).to.have.lengthOf(3) | ||
337 | |||
338 | const v = videos.find(v => v.name === 'video user 2') | ||
339 | expect(v).to.be.undefined | ||
340 | } | ||
341 | }) | ||
342 | |||
343 | it('Should block a local account', async function () { | ||
344 | await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') | ||
345 | }) | ||
346 | |||
347 | it('Should hide its videos', async function () { | ||
348 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
349 | const res = await getVideosListWithToken(servers[ 0 ].url, token) | ||
350 | |||
351 | const videos: Video[] = res.body.data | ||
352 | expect(videos).to.have.lengthOf(2) | ||
353 | |||
354 | const v = videos.find(v => v.name === 'video user 1') | ||
355 | expect(v).to.be.undefined | ||
356 | } | ||
357 | }) | ||
358 | |||
359 | it('Should hide its comments', async function () { | ||
360 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
361 | const resThreads = await getVideoCommentThreads(servers[ 0 ].url, videoUUID1, 0, 5, '-createdAt', token) | ||
362 | |||
363 | const threads: VideoComment[] = resThreads.body.data | ||
364 | expect(threads).to.have.lengthOf(1) | ||
365 | expect(threads[ 0 ].totalReplies).to.equal(0) | ||
366 | |||
367 | const t = threads.find(t => t.text === 'comment user 1') | ||
368 | expect(t).to.be.undefined | ||
369 | |||
370 | for (const thread of threads) { | ||
371 | const res = await getVideoThreadComments(servers[ 0 ].url, videoUUID1, thread.id, token) | ||
372 | |||
373 | const tree: VideoCommentThreadTree = res.body | ||
374 | expect(tree.children).to.have.lengthOf(0) | ||
375 | } | ||
376 | } | ||
377 | }) | ||
378 | |||
379 | it('Should list blocked accounts', async function () { | ||
380 | { | ||
381 | const res = await getAccountBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') | ||
382 | const blocks: AccountBlock[] = res.body.data | ||
383 | |||
384 | expect(res.body.total).to.equal(2) | ||
385 | |||
386 | const block = blocks[ 0 ] | ||
387 | expect(block.byAccount.displayName).to.equal('peertube') | ||
388 | expect(block.byAccount.name).to.equal('peertube') | ||
389 | expect(block.blockedAccount.displayName).to.equal('user2') | ||
390 | expect(block.blockedAccount.name).to.equal('user2') | ||
391 | expect(block.blockedAccount.host).to.equal('localhost:9002') | ||
392 | } | ||
393 | |||
394 | { | ||
395 | const res = await getAccountBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 1, 2, 'createdAt') | ||
396 | const blocks: AccountBlock[] = res.body.data | ||
397 | |||
398 | expect(res.body.total).to.equal(2) | ||
399 | |||
400 | const block = blocks[ 0 ] | ||
401 | expect(block.byAccount.displayName).to.equal('peertube') | ||
402 | expect(block.byAccount.name).to.equal('peertube') | ||
403 | expect(block.blockedAccount.displayName).to.equal('user1') | ||
404 | expect(block.blockedAccount.name).to.equal('user1') | ||
405 | expect(block.blockedAccount.host).to.equal('localhost:9001') | ||
406 | } | ||
407 | }) | ||
408 | |||
409 | it('Should unblock the remote account', async function () { | ||
410 | await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') | ||
411 | }) | ||
412 | |||
413 | it('Should display its videos', async function () { | ||
414 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
415 | const res = await getVideosListWithToken(servers[ 0 ].url, token) | ||
416 | |||
417 | const videos: Video[] = res.body.data | ||
418 | expect(videos).to.have.lengthOf(3) | ||
419 | |||
420 | const v = videos.find(v => v.name === 'video user 2') | ||
421 | expect(v).not.to.be.undefined | ||
422 | } | ||
423 | }) | ||
424 | |||
425 | it('Should unblock the local account', async function () { | ||
426 | await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') | ||
427 | }) | ||
428 | |||
429 | it('Should display its comments', async function () { | ||
430 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
431 | await checkAllComments(servers[ 0 ].url, token, videoUUID1) | ||
432 | } | ||
433 | }) | ||
434 | }) | ||
435 | |||
436 | describe('When managing server blocklist', function () { | ||
437 | it('Should list all videos', async function () { | ||
438 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
439 | await checkAllVideos(servers[ 0 ].url, token) | ||
440 | } | ||
441 | }) | ||
442 | |||
443 | it('Should list the comments', async function () { | ||
444 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
445 | await checkAllComments(servers[ 0 ].url, token, videoUUID1) | ||
446 | } | ||
447 | }) | ||
448 | |||
449 | it('Should block a remote server', async function () { | ||
450 | await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') | ||
451 | }) | ||
452 | |||
453 | it('Should hide its videos', async function () { | ||
454 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
455 | const res1 = await getVideosList(servers[ 0 ].url) | ||
456 | const res2 = await getVideosListWithToken(servers[ 0 ].url, token) | ||
457 | |||
458 | for (const res of [ res1, res2 ]) { | ||
459 | const videos: Video[] = res.body.data | ||
460 | expect(videos).to.have.lengthOf(2) | ||
461 | |||
462 | const v1 = videos.find(v => v.name === 'video user 2') | ||
463 | const v2 = videos.find(v => v.name === 'video server 2') | ||
464 | |||
465 | expect(v1).to.be.undefined | ||
466 | expect(v2).to.be.undefined | ||
467 | } | ||
468 | } | ||
469 | }) | ||
470 | |||
471 | it('Should hide its comments') | ||
472 | |||
473 | it('Should list blocked servers', async function () { | ||
474 | const res = await getServerBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') | ||
475 | const blocks: ServerBlock[] = res.body.data | ||
476 | |||
477 | expect(res.body.total).to.equal(1) | ||
478 | |||
479 | const block = blocks[ 0 ] | ||
480 | expect(block.byAccount.displayName).to.equal('peertube') | ||
481 | expect(block.byAccount.name).to.equal('peertube') | ||
482 | expect(block.blockedServer.host).to.equal('localhost:9002') | ||
483 | }) | ||
484 | |||
485 | it('Should unblock the remote server', async function () { | ||
486 | await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') | ||
487 | }) | ||
488 | |||
489 | it('Should list all videos', async function () { | ||
490 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
491 | await checkAllVideos(servers[ 0 ].url, token) | ||
492 | } | ||
493 | }) | ||
494 | |||
495 | it('Should list the comments', async function () { | ||
496 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
497 | await checkAllComments(servers[ 0 ].url, token, videoUUID1) | ||
498 | } | ||
499 | }) | ||
500 | }) | ||
501 | }) | ||
502 | |||
503 | after(async function () { | ||
504 | killallServers(servers) | ||
505 | |||
506 | // Keep the logs if the test failed | ||
507 | if (this[ 'ok' ]) { | ||
508 | await flushTests() | ||
509 | } | ||
510 | }) | ||
511 | }) | ||
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts index 21d75da3e..52ba6984e 100644 --- a/server/tests/api/users/index.ts +++ b/server/tests/api/users/index.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import './users-verification' | ||
2 | import './user-notifications' | ||
3 | import './blocklist' | ||
1 | import './user-subscriptions' | 4 | import './user-subscriptions' |
2 | import './users' | 5 | import './users' |
3 | import './users-verification' | ||
4 | import './users-multiple-servers' | 6 | import './users-multiple-servers' |
diff --git a/server/tests/api/users/user-notifications.ts b/server/tests/api/users/user-notifications.ts new file mode 100644 index 000000000..69e51677e --- /dev/null +++ b/server/tests/api/users/user-notifications.ts | |||
@@ -0,0 +1,1053 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | addVideoToBlacklist, | ||
7 | createUser, | ||
8 | doubleFollow, | ||
9 | flushAndRunMultipleServers, | ||
10 | flushTests, | ||
11 | getMyUserInformation, | ||
12 | immutableAssign, | ||
13 | registerUser, | ||
14 | removeVideoFromBlacklist, | ||
15 | reportVideoAbuse, | ||
16 | updateMyUser, | ||
17 | updateVideo, | ||
18 | updateVideoChannel, | ||
19 | userLogin, | ||
20 | wait | ||
21 | } from '../../../../shared/utils' | ||
22 | import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index' | ||
23 | import { setAccessTokensToServers } from '../../../../shared/utils/users/login' | ||
24 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
25 | import { getUserNotificationSocket } from '../../../../shared/utils/socket/socket-io' | ||
26 | import { | ||
27 | checkCommentMention, | ||
28 | CheckerBaseParams, | ||
29 | checkMyVideoImportIsFinished, | ||
30 | checkNewActorFollow, | ||
31 | checkNewBlacklistOnMyVideo, | ||
32 | checkNewCommentOnMyVideo, | ||
33 | checkNewVideoAbuseForModerators, | ||
34 | checkNewVideoFromSubscription, | ||
35 | checkUserRegistered, | ||
36 | checkVideoIsPublished, | ||
37 | getLastNotification, | ||
38 | getUserNotifications, | ||
39 | markAsReadNotifications, | ||
40 | updateMyNotificationSettings, | ||
41 | markAsReadAllNotifications | ||
42 | } from '../../../../shared/utils/users/user-notifications' | ||
43 | import { | ||
44 | User, | ||
45 | UserNotification, | ||
46 | UserNotificationSetting, | ||
47 | UserNotificationSettingValue, | ||
48 | UserNotificationType | ||
49 | } from '../../../../shared/models/users' | ||
50 | import { MockSmtpServer } from '../../../../shared/utils/miscs/email' | ||
51 | import { addUserSubscription, removeUserSubscription } from '../../../../shared/utils/users/user-subscriptions' | ||
52 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
53 | import { getBadVideoUrl, getYoutubeVideoUrl, importVideo } from '../../../../shared/utils/videos/video-imports' | ||
54 | import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments' | ||
55 | import * as uuidv4 from 'uuid/v4' | ||
56 | import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist' | ||
57 | |||
58 | const expect = chai.expect | ||
59 | |||
60 | async function uploadVideoByRemoteAccount (servers: ServerInfo[], additionalParams: any = {}) { | ||
61 | const name = 'remote video ' + uuidv4() | ||
62 | |||
63 | const data = Object.assign({ name }, additionalParams) | ||
64 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, data) | ||
65 | |||
66 | await waitJobs(servers) | ||
67 | |||
68 | return { uuid: res.body.video.uuid, name } | ||
69 | } | ||
70 | |||
71 | async function uploadVideoByLocalAccount (servers: ServerInfo[], additionalParams: any = {}) { | ||
72 | const name = 'local video ' + uuidv4() | ||
73 | |||
74 | const data = Object.assign({ name }, additionalParams) | ||
75 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, data) | ||
76 | |||
77 | await waitJobs(servers) | ||
78 | |||
79 | return { uuid: res.body.video.uuid, name } | ||
80 | } | ||
81 | |||
82 | describe('Test users notifications', function () { | ||
83 | let servers: ServerInfo[] = [] | ||
84 | let userAccessToken: string | ||
85 | let userNotifications: UserNotification[] = [] | ||
86 | let adminNotifications: UserNotification[] = [] | ||
87 | let adminNotificationsServer2: UserNotification[] = [] | ||
88 | const emails: object[] = [] | ||
89 | let channelId: number | ||
90 | |||
91 | const allNotificationSettings: UserNotificationSetting = { | ||
92 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
93 | newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
94 | videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
95 | blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
96 | myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
97 | myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
98 | commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
99 | newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
100 | newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | ||
101 | } | ||
102 | |||
103 | before(async function () { | ||
104 | this.timeout(120000) | ||
105 | |||
106 | await MockSmtpServer.Instance.collectEmails(emails) | ||
107 | |||
108 | await flushTests() | ||
109 | |||
110 | const overrideConfig = { | ||
111 | smtp: { | ||
112 | hostname: 'localhost' | ||
113 | } | ||
114 | } | ||
115 | servers = await flushAndRunMultipleServers(2, overrideConfig) | ||
116 | |||
117 | // Get the access tokens | ||
118 | await setAccessTokensToServers(servers) | ||
119 | |||
120 | // Server 1 and server 2 follow each other | ||
121 | await doubleFollow(servers[0], servers[1]) | ||
122 | |||
123 | await waitJobs(servers) | ||
124 | |||
125 | const user = { | ||
126 | username: 'user_1', | ||
127 | password: 'super password' | ||
128 | } | ||
129 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password, 10 * 1000 * 1000) | ||
130 | userAccessToken = await userLogin(servers[0], user) | ||
131 | |||
132 | await updateMyNotificationSettings(servers[0].url, userAccessToken, allNotificationSettings) | ||
133 | await updateMyNotificationSettings(servers[0].url, servers[0].accessToken, allNotificationSettings) | ||
134 | await updateMyNotificationSettings(servers[1].url, servers[1].accessToken, allNotificationSettings) | ||
135 | |||
136 | { | ||
137 | const socket = getUserNotificationSocket(servers[ 0 ].url, userAccessToken) | ||
138 | socket.on('new-notification', n => userNotifications.push(n)) | ||
139 | } | ||
140 | { | ||
141 | const socket = getUserNotificationSocket(servers[ 0 ].url, servers[0].accessToken) | ||
142 | socket.on('new-notification', n => adminNotifications.push(n)) | ||
143 | } | ||
144 | { | ||
145 | const socket = getUserNotificationSocket(servers[ 1 ].url, servers[1].accessToken) | ||
146 | socket.on('new-notification', n => adminNotificationsServer2.push(n)) | ||
147 | } | ||
148 | |||
149 | { | ||
150 | const resChannel = await getMyUserInformation(servers[0].url, servers[0].accessToken) | ||
151 | channelId = resChannel.body.videoChannels[0].id | ||
152 | } | ||
153 | }) | ||
154 | |||
155 | describe('New video from my subscription notification', function () { | ||
156 | let baseParams: CheckerBaseParams | ||
157 | |||
158 | before(() => { | ||
159 | baseParams = { | ||
160 | server: servers[0], | ||
161 | emails, | ||
162 | socketNotifications: userNotifications, | ||
163 | token: userAccessToken | ||
164 | } | ||
165 | }) | ||
166 | |||
167 | it('Should not send notifications if the user does not follow the video publisher', async function () { | ||
168 | this.timeout(10000) | ||
169 | |||
170 | await uploadVideoByLocalAccount(servers) | ||
171 | |||
172 | const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) | ||
173 | expect(notification).to.be.undefined | ||
174 | |||
175 | expect(emails).to.have.lengthOf(0) | ||
176 | expect(userNotifications).to.have.lengthOf(0) | ||
177 | }) | ||
178 | |||
179 | it('Should send a new video notification if the user follows the local video publisher', async function () { | ||
180 | this.timeout(15000) | ||
181 | |||
182 | await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9001') | ||
183 | await waitJobs(servers) | ||
184 | |||
185 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
186 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
187 | }) | ||
188 | |||
189 | it('Should send a new video notification from a remote account', async function () { | ||
190 | this.timeout(50000) // Server 2 has transcoding enabled | ||
191 | |||
192 | await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9002') | ||
193 | await waitJobs(servers) | ||
194 | |||
195 | const { name, uuid } = await uploadVideoByRemoteAccount(servers) | ||
196 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
197 | }) | ||
198 | |||
199 | it('Should send a new video notification on a scheduled publication', async function () { | ||
200 | this.timeout(20000) | ||
201 | |||
202 | // In 2 seconds | ||
203 | let updateAt = new Date(new Date().getTime() + 2000) | ||
204 | |||
205 | const data = { | ||
206 | privacy: VideoPrivacy.PRIVATE, | ||
207 | scheduleUpdate: { | ||
208 | updateAt: updateAt.toISOString(), | ||
209 | privacy: VideoPrivacy.PUBLIC | ||
210 | } | ||
211 | } | ||
212 | const { name, uuid } = await uploadVideoByLocalAccount(servers, data) | ||
213 | |||
214 | await wait(6000) | ||
215 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
216 | }) | ||
217 | |||
218 | it('Should send a new video notification on a remote scheduled publication', async function () { | ||
219 | this.timeout(20000) | ||
220 | |||
221 | // In 2 seconds | ||
222 | let updateAt = new Date(new Date().getTime() + 2000) | ||
223 | |||
224 | const data = { | ||
225 | privacy: VideoPrivacy.PRIVATE, | ||
226 | scheduleUpdate: { | ||
227 | updateAt: updateAt.toISOString(), | ||
228 | privacy: VideoPrivacy.PUBLIC | ||
229 | } | ||
230 | } | ||
231 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
232 | await waitJobs(servers) | ||
233 | |||
234 | await wait(6000) | ||
235 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
236 | }) | ||
237 | |||
238 | it('Should not send a notification before the video is published', async function () { | ||
239 | this.timeout(20000) | ||
240 | |||
241 | let updateAt = new Date(new Date().getTime() + 100000) | ||
242 | |||
243 | const data = { | ||
244 | privacy: VideoPrivacy.PRIVATE, | ||
245 | scheduleUpdate: { | ||
246 | updateAt: updateAt.toISOString(), | ||
247 | privacy: VideoPrivacy.PUBLIC | ||
248 | } | ||
249 | } | ||
250 | const { name, uuid } = await uploadVideoByLocalAccount(servers, data) | ||
251 | |||
252 | await wait(6000) | ||
253 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') | ||
254 | }) | ||
255 | |||
256 | it('Should send a new video notification when a video becomes public', async function () { | ||
257 | this.timeout(10000) | ||
258 | |||
259 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
260 | const { name, uuid } = await uploadVideoByLocalAccount(servers, data) | ||
261 | |||
262 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') | ||
263 | |||
264 | await updateVideo(servers[0].url, servers[0].accessToken, uuid, { privacy: VideoPrivacy.PUBLIC }) | ||
265 | |||
266 | await wait(500) | ||
267 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
268 | }) | ||
269 | |||
270 | it('Should send a new video notification when a remote video becomes public', async function () { | ||
271 | this.timeout(20000) | ||
272 | |||
273 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
274 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
275 | |||
276 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') | ||
277 | |||
278 | await updateVideo(servers[1].url, servers[1].accessToken, uuid, { privacy: VideoPrivacy.PUBLIC }) | ||
279 | |||
280 | await waitJobs(servers) | ||
281 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
282 | }) | ||
283 | |||
284 | it('Should not send a new video notification when a video becomes unlisted', async function () { | ||
285 | this.timeout(20000) | ||
286 | |||
287 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
288 | const { name, uuid } = await uploadVideoByLocalAccount(servers, data) | ||
289 | |||
290 | await updateVideo(servers[0].url, servers[0].accessToken, uuid, { privacy: VideoPrivacy.UNLISTED }) | ||
291 | |||
292 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') | ||
293 | }) | ||
294 | |||
295 | it('Should not send a new video notification when a remote video becomes unlisted', async function () { | ||
296 | this.timeout(20000) | ||
297 | |||
298 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
299 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
300 | |||
301 | await updateVideo(servers[1].url, servers[1].accessToken, uuid, { privacy: VideoPrivacy.UNLISTED }) | ||
302 | |||
303 | await waitJobs(servers) | ||
304 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') | ||
305 | }) | ||
306 | |||
307 | it('Should send a new video notification after a video import', async function () { | ||
308 | this.timeout(30000) | ||
309 | |||
310 | const name = 'video import ' + uuidv4() | ||
311 | |||
312 | const attributes = { | ||
313 | name, | ||
314 | channelId, | ||
315 | privacy: VideoPrivacy.PUBLIC, | ||
316 | targetUrl: getYoutubeVideoUrl() | ||
317 | } | ||
318 | const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) | ||
319 | const uuid = res.body.video.uuid | ||
320 | |||
321 | await waitJobs(servers) | ||
322 | |||
323 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
324 | }) | ||
325 | }) | ||
326 | |||
327 | describe('Comment on my video notifications', function () { | ||
328 | let baseParams: CheckerBaseParams | ||
329 | |||
330 | before(() => { | ||
331 | baseParams = { | ||
332 | server: servers[0], | ||
333 | emails, | ||
334 | socketNotifications: userNotifications, | ||
335 | token: userAccessToken | ||
336 | } | ||
337 | }) | ||
338 | |||
339 | it('Should not send a new comment notification after a comment on another video', async function () { | ||
340 | this.timeout(10000) | ||
341 | |||
342 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
343 | const uuid = resVideo.body.video.uuid | ||
344 | |||
345 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
346 | const commentId = resComment.body.comment.id | ||
347 | |||
348 | await wait(500) | ||
349 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') | ||
350 | }) | ||
351 | |||
352 | it('Should not send a new comment notification if I comment my own video', async function () { | ||
353 | this.timeout(10000) | ||
354 | |||
355 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
356 | const uuid = resVideo.body.video.uuid | ||
357 | |||
358 | const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, uuid, 'comment') | ||
359 | const commentId = resComment.body.comment.id | ||
360 | |||
361 | await wait(500) | ||
362 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') | ||
363 | }) | ||
364 | |||
365 | it('Should not send a new comment notification if the account is muted', async function () { | ||
366 | this.timeout(10000) | ||
367 | |||
368 | await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') | ||
369 | |||
370 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
371 | const uuid = resVideo.body.video.uuid | ||
372 | |||
373 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
374 | const commentId = resComment.body.comment.id | ||
375 | |||
376 | await wait(500) | ||
377 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') | ||
378 | |||
379 | await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') | ||
380 | }) | ||
381 | |||
382 | it('Should send a new comment notification after a local comment on my video', async function () { | ||
383 | this.timeout(10000) | ||
384 | |||
385 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
386 | const uuid = resVideo.body.video.uuid | ||
387 | |||
388 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
389 | const commentId = resComment.body.comment.id | ||
390 | |||
391 | await wait(500) | ||
392 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'presence') | ||
393 | }) | ||
394 | |||
395 | it('Should send a new comment notification after a remote comment on my video', async function () { | ||
396 | this.timeout(10000) | ||
397 | |||
398 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
399 | const uuid = resVideo.body.video.uuid | ||
400 | |||
401 | await waitJobs(servers) | ||
402 | |||
403 | const resComment = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment') | ||
404 | const commentId = resComment.body.comment.id | ||
405 | |||
406 | await waitJobs(servers) | ||
407 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'presence') | ||
408 | }) | ||
409 | |||
410 | it('Should send a new comment notification after a local reply on my video', async function () { | ||
411 | this.timeout(10000) | ||
412 | |||
413 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
414 | const uuid = resVideo.body.video.uuid | ||
415 | |||
416 | const resThread = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
417 | const threadId = resThread.body.comment.id | ||
418 | |||
419 | const resComment = await addVideoCommentReply(servers[0].url, servers[0].accessToken, uuid, threadId, 'reply') | ||
420 | const commentId = resComment.body.comment.id | ||
421 | |||
422 | await wait(500) | ||
423 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, threadId, 'presence') | ||
424 | }) | ||
425 | |||
426 | it('Should send a new comment notification after a remote reply on my video', async function () { | ||
427 | this.timeout(10000) | ||
428 | |||
429 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
430 | const uuid = resVideo.body.video.uuid | ||
431 | await waitJobs(servers) | ||
432 | |||
433 | const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment') | ||
434 | const threadId = resThread.body.comment.id | ||
435 | |||
436 | const resComment = await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, threadId, 'reply') | ||
437 | const commentId = resComment.body.comment.id | ||
438 | |||
439 | await waitJobs(servers) | ||
440 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, threadId, 'presence') | ||
441 | }) | ||
442 | }) | ||
443 | |||
444 | describe('Mention notifications', function () { | ||
445 | let baseParams: CheckerBaseParams | ||
446 | |||
447 | before(async () => { | ||
448 | baseParams = { | ||
449 | server: servers[0], | ||
450 | emails, | ||
451 | socketNotifications: userNotifications, | ||
452 | token: userAccessToken | ||
453 | } | ||
454 | |||
455 | await updateMyUser({ | ||
456 | url: servers[0].url, | ||
457 | accessToken: servers[0].accessToken, | ||
458 | displayName: 'super root name' | ||
459 | }) | ||
460 | |||
461 | await updateMyUser({ | ||
462 | url: servers[1].url, | ||
463 | accessToken: servers[1].accessToken, | ||
464 | displayName: 'super root 2 name' | ||
465 | }) | ||
466 | }) | ||
467 | |||
468 | it('Should not send a new mention comment notification if I mention the video owner', async function () { | ||
469 | this.timeout(10000) | ||
470 | |||
471 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
472 | const uuid = resVideo.body.video.uuid | ||
473 | |||
474 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello') | ||
475 | const commentId = resComment.body.comment.id | ||
476 | |||
477 | await wait(500) | ||
478 | await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence') | ||
479 | }) | ||
480 | |||
481 | it('Should not send a new mention comment notification if I mention myself', async function () { | ||
482 | this.timeout(10000) | ||
483 | |||
484 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
485 | const uuid = resVideo.body.video.uuid | ||
486 | |||
487 | const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, uuid, '@user_1 hello') | ||
488 | const commentId = resComment.body.comment.id | ||
489 | |||
490 | await wait(500) | ||
491 | await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence') | ||
492 | }) | ||
493 | |||
494 | it('Should not send a new mention notification if the account is muted', async function () { | ||
495 | this.timeout(10000) | ||
496 | |||
497 | await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') | ||
498 | |||
499 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
500 | const uuid = resVideo.body.video.uuid | ||
501 | |||
502 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello') | ||
503 | const commentId = resComment.body.comment.id | ||
504 | |||
505 | await wait(500) | ||
506 | await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence') | ||
507 | |||
508 | await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') | ||
509 | }) | ||
510 | |||
511 | it('Should send a new mention notification after local comments', async function () { | ||
512 | this.timeout(10000) | ||
513 | |||
514 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
515 | const uuid = resVideo.body.video.uuid | ||
516 | |||
517 | const resThread = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello 1') | ||
518 | const threadId = resThread.body.comment.id | ||
519 | |||
520 | await wait(500) | ||
521 | await checkCommentMention(baseParams, uuid, threadId, threadId, 'super root name', 'presence') | ||
522 | |||
523 | const resComment = await addVideoCommentReply(servers[0].url, servers[0].accessToken, uuid, threadId, 'hello 2 @user_1') | ||
524 | const commentId = resComment.body.comment.id | ||
525 | |||
526 | await wait(500) | ||
527 | await checkCommentMention(baseParams, uuid, commentId, threadId, 'super root name', 'presence') | ||
528 | }) | ||
529 | |||
530 | it('Should send a new mention notification after remote comments', async function () { | ||
531 | this.timeout(20000) | ||
532 | |||
533 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
534 | const uuid = resVideo.body.video.uuid | ||
535 | |||
536 | await waitJobs(servers) | ||
537 | const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'hello @user_1@localhost:9001 1') | ||
538 | const threadId = resThread.body.comment.id | ||
539 | |||
540 | await waitJobs(servers) | ||
541 | await checkCommentMention(baseParams, uuid, threadId, threadId, 'super root 2 name', 'presence') | ||
542 | |||
543 | const text = '@user_1@localhost:9001 hello 2 @root@localhost:9001' | ||
544 | const resComment = await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, threadId, text) | ||
545 | const commentId = resComment.body.comment.id | ||
546 | |||
547 | await waitJobs(servers) | ||
548 | await checkCommentMention(baseParams, uuid, commentId, threadId, 'super root 2 name', 'presence') | ||
549 | }) | ||
550 | }) | ||
551 | |||
552 | describe('Video abuse for moderators notification' , function () { | ||
553 | let baseParams: CheckerBaseParams | ||
554 | |||
555 | before(() => { | ||
556 | baseParams = { | ||
557 | server: servers[0], | ||
558 | emails, | ||
559 | socketNotifications: adminNotifications, | ||
560 | token: servers[0].accessToken | ||
561 | } | ||
562 | }) | ||
563 | |||
564 | it('Should send a notification to moderators on local video abuse', async function () { | ||
565 | this.timeout(10000) | ||
566 | |||
567 | const name = 'video for abuse ' + uuidv4() | ||
568 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | ||
569 | const uuid = resVideo.body.video.uuid | ||
570 | |||
571 | await reportVideoAbuse(servers[0].url, servers[0].accessToken, uuid, 'super reason') | ||
572 | |||
573 | await waitJobs(servers) | ||
574 | await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') | ||
575 | }) | ||
576 | |||
577 | it('Should send a notification to moderators on remote video abuse', async function () { | ||
578 | this.timeout(10000) | ||
579 | |||
580 | const name = 'video for abuse ' + uuidv4() | ||
581 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | ||
582 | const uuid = resVideo.body.video.uuid | ||
583 | |||
584 | await waitJobs(servers) | ||
585 | |||
586 | await reportVideoAbuse(servers[1].url, servers[1].accessToken, uuid, 'super reason') | ||
587 | |||
588 | await waitJobs(servers) | ||
589 | await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') | ||
590 | }) | ||
591 | }) | ||
592 | |||
593 | describe('Video blacklist on my video', function () { | ||
594 | let baseParams: CheckerBaseParams | ||
595 | |||
596 | before(() => { | ||
597 | baseParams = { | ||
598 | server: servers[0], | ||
599 | emails, | ||
600 | socketNotifications: userNotifications, | ||
601 | token: userAccessToken | ||
602 | } | ||
603 | }) | ||
604 | |||
605 | it('Should send a notification to video owner on blacklist', async function () { | ||
606 | this.timeout(10000) | ||
607 | |||
608 | const name = 'video for abuse ' + uuidv4() | ||
609 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | ||
610 | const uuid = resVideo.body.video.uuid | ||
611 | |||
612 | await addVideoToBlacklist(servers[0].url, servers[0].accessToken, uuid) | ||
613 | |||
614 | await waitJobs(servers) | ||
615 | await checkNewBlacklistOnMyVideo(baseParams, uuid, name, 'blacklist') | ||
616 | }) | ||
617 | |||
618 | it('Should send a notification to video owner on unblacklist', async function () { | ||
619 | this.timeout(10000) | ||
620 | |||
621 | const name = 'video for abuse ' + uuidv4() | ||
622 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | ||
623 | const uuid = resVideo.body.video.uuid | ||
624 | |||
625 | await addVideoToBlacklist(servers[0].url, servers[0].accessToken, uuid) | ||
626 | |||
627 | await waitJobs(servers) | ||
628 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, uuid) | ||
629 | await waitJobs(servers) | ||
630 | |||
631 | await wait(500) | ||
632 | await checkNewBlacklistOnMyVideo(baseParams, uuid, name, 'unblacklist') | ||
633 | }) | ||
634 | }) | ||
635 | |||
636 | describe('My video is published', function () { | ||
637 | let baseParams: CheckerBaseParams | ||
638 | |||
639 | before(() => { | ||
640 | baseParams = { | ||
641 | server: servers[1], | ||
642 | emails, | ||
643 | socketNotifications: adminNotificationsServer2, | ||
644 | token: servers[1].accessToken | ||
645 | } | ||
646 | }) | ||
647 | |||
648 | it('Should not send a notification if transcoding is not enabled', async function () { | ||
649 | this.timeout(10000) | ||
650 | |||
651 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
652 | await waitJobs(servers) | ||
653 | |||
654 | await checkVideoIsPublished(baseParams, name, uuid, 'absence') | ||
655 | }) | ||
656 | |||
657 | it('Should not send a notification if the wait transcoding is false', async function () { | ||
658 | this.timeout(50000) | ||
659 | |||
660 | await uploadVideoByRemoteAccount(servers, { waitTranscoding: false }) | ||
661 | await waitJobs(servers) | ||
662 | |||
663 | const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) | ||
664 | if (notification) { | ||
665 | expect(notification.type).to.not.equal(UserNotificationType.MY_VIDEO_PUBLISHED) | ||
666 | } | ||
667 | }) | ||
668 | |||
669 | it('Should send a notification even if the video is not transcoded in other resolutions', async function () { | ||
670 | this.timeout(50000) | ||
671 | |||
672 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, { waitTranscoding: true, fixture: 'video_short_240p.mp4' }) | ||
673 | await waitJobs(servers) | ||
674 | |||
675 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') | ||
676 | }) | ||
677 | |||
678 | it('Should send a notification with a transcoded video', async function () { | ||
679 | this.timeout(50000) | ||
680 | |||
681 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, { waitTranscoding: true }) | ||
682 | await waitJobs(servers) | ||
683 | |||
684 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') | ||
685 | }) | ||
686 | |||
687 | it('Should send a notification when an imported video is transcoded', async function () { | ||
688 | this.timeout(50000) | ||
689 | |||
690 | const name = 'video import ' + uuidv4() | ||
691 | |||
692 | const attributes = { | ||
693 | name, | ||
694 | channelId, | ||
695 | privacy: VideoPrivacy.PUBLIC, | ||
696 | targetUrl: getYoutubeVideoUrl(), | ||
697 | waitTranscoding: true | ||
698 | } | ||
699 | const res = await importVideo(servers[1].url, servers[1].accessToken, attributes) | ||
700 | const uuid = res.body.video.uuid | ||
701 | |||
702 | await waitJobs(servers) | ||
703 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') | ||
704 | }) | ||
705 | |||
706 | it('Should send a notification when the scheduled update has been proceeded', async function () { | ||
707 | this.timeout(70000) | ||
708 | |||
709 | // In 2 seconds | ||
710 | let updateAt = new Date(new Date().getTime() + 2000) | ||
711 | |||
712 | const data = { | ||
713 | privacy: VideoPrivacy.PRIVATE, | ||
714 | scheduleUpdate: { | ||
715 | updateAt: updateAt.toISOString(), | ||
716 | privacy: VideoPrivacy.PUBLIC | ||
717 | } | ||
718 | } | ||
719 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
720 | |||
721 | await wait(6000) | ||
722 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') | ||
723 | }) | ||
724 | |||
725 | it('Should not send a notification before the video is published', async function () { | ||
726 | this.timeout(20000) | ||
727 | |||
728 | let updateAt = new Date(new Date().getTime() + 100000) | ||
729 | |||
730 | const data = { | ||
731 | privacy: VideoPrivacy.PRIVATE, | ||
732 | scheduleUpdate: { | ||
733 | updateAt: updateAt.toISOString(), | ||
734 | privacy: VideoPrivacy.PUBLIC | ||
735 | } | ||
736 | } | ||
737 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
738 | |||
739 | await wait(6000) | ||
740 | await checkVideoIsPublished(baseParams, name, uuid, 'absence') | ||
741 | }) | ||
742 | }) | ||
743 | |||
744 | describe('My video is imported', function () { | ||
745 | let baseParams: CheckerBaseParams | ||
746 | |||
747 | before(() => { | ||
748 | baseParams = { | ||
749 | server: servers[0], | ||
750 | emails, | ||
751 | socketNotifications: adminNotifications, | ||
752 | token: servers[0].accessToken | ||
753 | } | ||
754 | }) | ||
755 | |||
756 | it('Should send a notification when the video import failed', async function () { | ||
757 | this.timeout(70000) | ||
758 | |||
759 | const name = 'video import ' + uuidv4() | ||
760 | |||
761 | const attributes = { | ||
762 | name, | ||
763 | channelId, | ||
764 | privacy: VideoPrivacy.PRIVATE, | ||
765 | targetUrl: getBadVideoUrl() | ||
766 | } | ||
767 | const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) | ||
768 | const uuid = res.body.video.uuid | ||
769 | |||
770 | await waitJobs(servers) | ||
771 | await checkMyVideoImportIsFinished(baseParams, name, uuid, getBadVideoUrl(), false, 'presence') | ||
772 | }) | ||
773 | |||
774 | it('Should send a notification when the video import succeeded', async function () { | ||
775 | this.timeout(70000) | ||
776 | |||
777 | const name = 'video import ' + uuidv4() | ||
778 | |||
779 | const attributes = { | ||
780 | name, | ||
781 | channelId, | ||
782 | privacy: VideoPrivacy.PRIVATE, | ||
783 | targetUrl: getYoutubeVideoUrl() | ||
784 | } | ||
785 | const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) | ||
786 | const uuid = res.body.video.uuid | ||
787 | |||
788 | await waitJobs(servers) | ||
789 | await checkMyVideoImportIsFinished(baseParams, name, uuid, getYoutubeVideoUrl(), true, 'presence') | ||
790 | }) | ||
791 | }) | ||
792 | |||
793 | describe('New registration', function () { | ||
794 | let baseParams: CheckerBaseParams | ||
795 | |||
796 | before(() => { | ||
797 | baseParams = { | ||
798 | server: servers[0], | ||
799 | emails, | ||
800 | socketNotifications: adminNotifications, | ||
801 | token: servers[0].accessToken | ||
802 | } | ||
803 | }) | ||
804 | |||
805 | it('Should send a notification only to moderators when a user registers on the instance', async function () { | ||
806 | this.timeout(10000) | ||
807 | |||
808 | await registerUser(servers[0].url, 'user_45', 'password') | ||
809 | |||
810 | await waitJobs(servers) | ||
811 | |||
812 | await checkUserRegistered(baseParams, 'user_45', 'presence') | ||
813 | |||
814 | const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } } | ||
815 | await checkUserRegistered(immutableAssign(baseParams, userOverride), 'user_45', 'absence') | ||
816 | }) | ||
817 | }) | ||
818 | |||
819 | describe('New actor follow', function () { | ||
820 | let baseParams: CheckerBaseParams | ||
821 | let myChannelName = 'super channel name' | ||
822 | let myUserName = 'super user name' | ||
823 | |||
824 | before(async () => { | ||
825 | baseParams = { | ||
826 | server: servers[0], | ||
827 | emails, | ||
828 | socketNotifications: userNotifications, | ||
829 | token: userAccessToken | ||
830 | } | ||
831 | |||
832 | await updateMyUser({ | ||
833 | url: servers[0].url, | ||
834 | accessToken: servers[0].accessToken, | ||
835 | displayName: 'super root name' | ||
836 | }) | ||
837 | |||
838 | await updateMyUser({ | ||
839 | url: servers[0].url, | ||
840 | accessToken: userAccessToken, | ||
841 | displayName: myUserName | ||
842 | }) | ||
843 | |||
844 | await updateMyUser({ | ||
845 | url: servers[1].url, | ||
846 | accessToken: servers[1].accessToken, | ||
847 | displayName: 'super root 2 name' | ||
848 | }) | ||
849 | |||
850 | await updateVideoChannel(servers[0].url, userAccessToken, 'user_1_channel', { displayName: myChannelName }) | ||
851 | }) | ||
852 | |||
853 | it('Should notify when a local channel is following one of our channel', async function () { | ||
854 | this.timeout(10000) | ||
855 | |||
856 | await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001') | ||
857 | await waitJobs(servers) | ||
858 | |||
859 | await checkNewActorFollow(baseParams, 'channel', 'root', 'super root name', myChannelName, 'presence') | ||
860 | |||
861 | await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001') | ||
862 | }) | ||
863 | |||
864 | it('Should notify when a remote channel is following one of our channel', async function () { | ||
865 | this.timeout(10000) | ||
866 | |||
867 | await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001') | ||
868 | await waitJobs(servers) | ||
869 | |||
870 | await checkNewActorFollow(baseParams, 'channel', 'root', 'super root 2 name', myChannelName, 'presence') | ||
871 | |||
872 | await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001') | ||
873 | }) | ||
874 | |||
875 | it('Should notify when a local account is following one of our channel', async function () { | ||
876 | this.timeout(10000) | ||
877 | |||
878 | await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:9001') | ||
879 | |||
880 | await waitJobs(servers) | ||
881 | |||
882 | await checkNewActorFollow(baseParams, 'account', 'root', 'super root name', myUserName, 'presence') | ||
883 | }) | ||
884 | |||
885 | it('Should notify when a remote account is following one of our channel', async function () { | ||
886 | this.timeout(10000) | ||
887 | |||
888 | await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:9001') | ||
889 | |||
890 | await waitJobs(servers) | ||
891 | |||
892 | await checkNewActorFollow(baseParams, 'account', 'root', 'super root 2 name', myUserName, 'presence') | ||
893 | }) | ||
894 | }) | ||
895 | |||
896 | describe('Mark as read', function () { | ||
897 | it('Should mark as read some notifications', async function () { | ||
898 | const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3) | ||
899 | const ids = res.body.data.map(n => n.id) | ||
900 | |||
901 | await markAsReadNotifications(servers[ 0 ].url, userAccessToken, ids) | ||
902 | }) | ||
903 | |||
904 | it('Should have the notifications marked as read', async function () { | ||
905 | const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10) | ||
906 | |||
907 | const notifications = res.body.data as UserNotification[] | ||
908 | expect(notifications[ 0 ].read).to.be.false | ||
909 | expect(notifications[ 1 ].read).to.be.false | ||
910 | expect(notifications[ 2 ].read).to.be.true | ||
911 | expect(notifications[ 3 ].read).to.be.true | ||
912 | expect(notifications[ 4 ].read).to.be.true | ||
913 | expect(notifications[ 5 ].read).to.be.false | ||
914 | }) | ||
915 | |||
916 | it('Should only list read notifications', async function () { | ||
917 | const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, false) | ||
918 | |||
919 | const notifications = res.body.data as UserNotification[] | ||
920 | for (const notification of notifications) { | ||
921 | expect(notification.read).to.be.true | ||
922 | } | ||
923 | }) | ||
924 | |||
925 | it('Should only list unread notifications', async function () { | ||
926 | const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, true) | ||
927 | |||
928 | const notifications = res.body.data as UserNotification[] | ||
929 | for (const notification of notifications) { | ||
930 | expect(notification.read).to.be.false | ||
931 | } | ||
932 | }) | ||
933 | |||
934 | it('Should mark as read all notifications', async function () { | ||
935 | await markAsReadAllNotifications(servers[ 0 ].url, userAccessToken) | ||
936 | |||
937 | const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, true) | ||
938 | |||
939 | expect(res.body.total).to.equal(0) | ||
940 | expect(res.body.data).to.have.lengthOf(0) | ||
941 | }) | ||
942 | }) | ||
943 | |||
944 | describe('Notification settings', function () { | ||
945 | let baseParams: CheckerBaseParams | ||
946 | |||
947 | before(() => { | ||
948 | baseParams = { | ||
949 | server: servers[0], | ||
950 | emails, | ||
951 | socketNotifications: userNotifications, | ||
952 | token: userAccessToken | ||
953 | } | ||
954 | }) | ||
955 | |||
956 | it('Should not have notifications', async function () { | ||
957 | this.timeout(10000) | ||
958 | |||
959 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | ||
960 | newVideoFromSubscription: UserNotificationSettingValue.NONE | ||
961 | })) | ||
962 | |||
963 | { | ||
964 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
965 | const info = res.body as User | ||
966 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.NONE) | ||
967 | } | ||
968 | |||
969 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
970 | |||
971 | const check = { web: true, mail: true } | ||
972 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence') | ||
973 | }) | ||
974 | |||
975 | it('Should only have web notifications', async function () { | ||
976 | this.timeout(10000) | ||
977 | |||
978 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | ||
979 | newVideoFromSubscription: UserNotificationSettingValue.WEB | ||
980 | })) | ||
981 | |||
982 | { | ||
983 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
984 | const info = res.body as User | ||
985 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB) | ||
986 | } | ||
987 | |||
988 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
989 | |||
990 | { | ||
991 | const check = { mail: true, web: false } | ||
992 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence') | ||
993 | } | ||
994 | |||
995 | { | ||
996 | const check = { mail: false, web: true } | ||
997 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'presence') | ||
998 | } | ||
999 | }) | ||
1000 | |||
1001 | it('Should only have mail notifications', async function () { | ||
1002 | this.timeout(10000) | ||
1003 | |||
1004 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | ||
1005 | newVideoFromSubscription: UserNotificationSettingValue.EMAIL | ||
1006 | })) | ||
1007 | |||
1008 | { | ||
1009 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
1010 | const info = res.body as User | ||
1011 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.EMAIL) | ||
1012 | } | ||
1013 | |||
1014 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
1015 | |||
1016 | { | ||
1017 | const check = { mail: false, web: true } | ||
1018 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence') | ||
1019 | } | ||
1020 | |||
1021 | { | ||
1022 | const check = { mail: true, web: false } | ||
1023 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'presence') | ||
1024 | } | ||
1025 | }) | ||
1026 | |||
1027 | it('Should have email and web notifications', async function () { | ||
1028 | this.timeout(10000) | ||
1029 | |||
1030 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | ||
1031 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | ||
1032 | })) | ||
1033 | |||
1034 | { | ||
1035 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
1036 | const info = res.body as User | ||
1037 | expect(info.notificationSettings.newVideoFromSubscription).to.equal( | ||
1038 | UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | ||
1039 | ) | ||
1040 | } | ||
1041 | |||
1042 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
1043 | |||
1044 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
1045 | }) | ||
1046 | }) | ||
1047 | |||
1048 | after(async function () { | ||
1049 | MockSmtpServer.Instance.kill() | ||
1050 | |||
1051 | killallServers(servers) | ||
1052 | }) | ||
1053 | }) | ||
diff --git a/server/tests/api/users/user-subscriptions.ts b/server/tests/api/users/user-subscriptions.ts index 65b80540c..88a7187d6 100644 --- a/server/tests/api/users/user-subscriptions.ts +++ b/server/tests/api/users/user-subscriptions.ts | |||
@@ -2,18 +2,27 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { createUser, doubleFollow, flushAndRunMultipleServers, follow, getVideosList, unfollow, updateVideo, userLogin } from '../../utils' | 5 | import { |
6 | import { killallServers, ServerInfo, uploadVideo } from '../../utils/index' | 6 | createUser, |
7 | import { setAccessTokensToServers } from '../../utils/users/login' | 7 | doubleFollow, |
8 | flushAndRunMultipleServers, | ||
9 | follow, | ||
10 | getVideosList, | ||
11 | unfollow, | ||
12 | updateVideo, | ||
13 | userLogin | ||
14 | } from '../../../../shared/utils' | ||
15 | import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index' | ||
16 | import { setAccessTokensToServers } from '../../../../shared/utils/users/login' | ||
8 | import { Video, VideoChannel } from '../../../../shared/models/videos' | 17 | import { Video, VideoChannel } from '../../../../shared/models/videos' |
9 | import { waitJobs } from '../../utils/server/jobs' | 18 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
10 | import { | 19 | import { |
11 | addUserSubscription, | 20 | addUserSubscription, |
12 | listUserSubscriptions, | 21 | listUserSubscriptions, |
13 | listUserSubscriptionVideos, | 22 | listUserSubscriptionVideos, |
14 | removeUserSubscription, | 23 | removeUserSubscription, |
15 | getUserSubscription, areSubscriptionsExist | 24 | getUserSubscription, areSubscriptionsExist |
16 | } from '../../utils/users/user-subscriptions' | 25 | } from '../../../../shared/utils/users/user-subscriptions' |
17 | 26 | ||
18 | const expect = chai.expect | 27 | const expect = chai.expect |
19 | 28 | ||
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts index d8699db17..006d6cdf0 100644 --- a/server/tests/api/users/users-multiple-servers.ts +++ b/server/tests/api/users/users-multiple-servers.ts | |||
@@ -13,13 +13,13 @@ import { | |||
13 | removeUser, | 13 | removeUser, |
14 | updateMyUser, | 14 | updateMyUser, |
15 | userLogin | 15 | userLogin |
16 | } from '../../utils' | 16 | } from '../../../../shared/utils' |
17 | import { getMyUserInformation, killallServers, ServerInfo, testImage, updateMyAvatar, uploadVideo } from '../../utils/index' | 17 | import { getMyUserInformation, killallServers, ServerInfo, testImage, updateMyAvatar, uploadVideo } from '../../../../shared/utils/index' |
18 | import { checkActorFilesWereRemoved, getAccount, getAccountsList } from '../../utils/users/accounts' | 18 | import { checkActorFilesWereRemoved, getAccount, getAccountsList } from '../../../../shared/utils/users/accounts' |
19 | import { setAccessTokensToServers } from '../../utils/users/login' | 19 | import { setAccessTokensToServers } from '../../../../shared/utils/users/login' |
20 | import { User } from '../../../../shared/models/users' | 20 | import { User } from '../../../../shared/models/users' |
21 | import { VideoChannel } from '../../../../shared/models/videos' | 21 | import { VideoChannel } from '../../../../shared/models/videos' |
22 | import { waitJobs } from '../../utils/server/jobs' | 22 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
23 | 23 | ||
24 | const expect = chai.expect | 24 | const expect = chai.expect |
25 | 25 | ||
diff --git a/server/tests/api/users/users-verification.ts b/server/tests/api/users/users-verification.ts index fa5f5e371..babeda2b8 100644 --- a/server/tests/api/users/users-verification.ts +++ b/server/tests/api/users/users-verification.ts | |||
@@ -4,11 +4,11 @@ import * as chai from 'chai' | |||
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { | 5 | import { |
6 | registerUser, flushTests, getUserInformation, getMyUserInformation, killallServers, | 6 | registerUser, flushTests, getUserInformation, getMyUserInformation, killallServers, |
7 | userLogin, login, runServer, ServerInfo, verifyEmail, updateCustomSubConfig | 7 | userLogin, login, runServer, ServerInfo, verifyEmail, updateCustomSubConfig, wait |
8 | } from '../../utils' | 8 | } from '../../../../shared/utils' |
9 | import { setAccessTokensToServers } from '../../utils/users/login' | 9 | import { setAccessTokensToServers } from '../../../../shared/utils/users/login' |
10 | import { mockSmtpServer } from '../../utils/miscs/email' | 10 | import { MockSmtpServer } from '../../../../shared/utils/miscs/email' |
11 | import { waitJobs } from '../../utils/server/jobs' | 11 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
12 | 12 | ||
13 | const expect = chai.expect | 13 | const expect = chai.expect |
14 | 14 | ||
@@ -30,7 +30,7 @@ describe('Test users account verification', function () { | |||
30 | before(async function () { | 30 | before(async function () { |
31 | this.timeout(30000) | 31 | this.timeout(30000) |
32 | 32 | ||
33 | await mockSmtpServer(emails) | 33 | await MockSmtpServer.Instance.collectEmails(emails) |
34 | 34 | ||
35 | await flushTests() | 35 | await flushTests() |
36 | 36 | ||
@@ -123,6 +123,7 @@ describe('Test users account verification', function () { | |||
123 | }) | 123 | }) |
124 | 124 | ||
125 | after(async function () { | 125 | after(async function () { |
126 | MockSmtpServer.Instance.kill() | ||
126 | killallServers([ server ]) | 127 | killallServers([ server ]) |
127 | 128 | ||
128 | // Keep the logs if the test failed | 129 | // Keep the logs if the test failed |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 8b9c6b455..c4465d541 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -32,10 +32,10 @@ import { | |||
32 | updateUser, | 32 | updateUser, |
33 | uploadVideo, | 33 | uploadVideo, |
34 | userLogin | 34 | userLogin |
35 | } from '../../utils/index' | 35 | } from '../../../../shared/utils/index' |
36 | import { follow } from '../../utils/server/follows' | 36 | import { follow } from '../../../../shared/utils/server/follows' |
37 | import { setAccessTokensToServers } from '../../utils/users/login' | 37 | import { setAccessTokensToServers } from '../../../../shared/utils/users/login' |
38 | import { getMyVideos } from '../../utils/videos/videos' | 38 | import { getMyVideos } from '../../../../shared/utils/videos/videos' |
39 | 39 | ||
40 | const expect = chai.expect | 40 | const expect = chai.expect |
41 | 41 | ||
@@ -180,7 +180,7 @@ describe('Test users', function () { | |||
180 | it('Should be able to upload a video again') | 180 | it('Should be able to upload a video again') |
181 | 181 | ||
182 | it('Should be able to create a new user', async function () { | 182 | it('Should be able to create a new user', async function () { |
183 | await createUser(server.url, accessToken, user.username,user.password, 2 * 1024 * 1024) | 183 | await createUser(server.url, accessToken, user.username, user.password, 2 * 1024 * 1024) |
184 | }) | 184 | }) |
185 | 185 | ||
186 | it('Should be able to login with this user', async function () { | 186 | it('Should be able to login with this user', async function () { |
@@ -322,6 +322,40 @@ describe('Test users', function () { | |||
322 | expect(users[ 1 ].nsfwPolicy).to.equal('display') | 322 | expect(users[ 1 ].nsfwPolicy).to.equal('display') |
323 | }) | 323 | }) |
324 | 324 | ||
325 | it('Should search user by username', async function () { | ||
326 | const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'oot') | ||
327 | const users = res.body.data as User[] | ||
328 | |||
329 | expect(res.body.total).to.equal(1) | ||
330 | expect(users.length).to.equal(1) | ||
331 | |||
332 | expect(users[ 0 ].username).to.equal('root') | ||
333 | }) | ||
334 | |||
335 | it('Should search user by email', async function () { | ||
336 | { | ||
337 | const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'r_1@exam') | ||
338 | const users = res.body.data as User[] | ||
339 | |||
340 | expect(res.body.total).to.equal(1) | ||
341 | expect(users.length).to.equal(1) | ||
342 | |||
343 | expect(users[ 0 ].username).to.equal('user_1') | ||
344 | expect(users[ 0 ].email).to.equal('user_1@example.com') | ||
345 | } | ||
346 | |||
347 | { | ||
348 | const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'example') | ||
349 | const users = res.body.data as User[] | ||
350 | |||
351 | expect(res.body.total).to.equal(2) | ||
352 | expect(users.length).to.equal(2) | ||
353 | |||
354 | expect(users[ 0 ].username).to.equal('root') | ||
355 | expect(users[ 1 ].username).to.equal('user_1') | ||
356 | } | ||
357 | }) | ||
358 | |||
325 | it('Should update my password', async function () { | 359 | it('Should update my password', async function () { |
326 | await updateMyUser({ | 360 | await updateMyUser({ |
327 | url: server.url, | 361 | url: server.url, |
@@ -444,6 +478,7 @@ describe('Test users', function () { | |||
444 | userId, | 478 | userId, |
445 | accessToken, | 479 | accessToken, |
446 | email: 'updated2@example.com', | 480 | email: 'updated2@example.com', |
481 | emailVerified: true, | ||
447 | videoQuota: 42, | 482 | videoQuota: 42, |
448 | role: UserRole.MODERATOR | 483 | role: UserRole.MODERATOR |
449 | }) | 484 | }) |
@@ -453,6 +488,7 @@ describe('Test users', function () { | |||
453 | 488 | ||
454 | expect(user.username).to.equal('user_1') | 489 | expect(user.username).to.equal('user_1') |
455 | expect(user.email).to.equal('updated2@example.com') | 490 | expect(user.email).to.equal('updated2@example.com') |
491 | expect(user.emailVerified).to.be.true | ||
456 | expect(user.nsfwPolicy).to.equal('do_not_list') | 492 | expect(user.nsfwPolicy).to.equal('do_not_list') |
457 | expect(user.videoQuota).to.equal(42) | 493 | expect(user.videoQuota).to.equal(42) |
458 | expect(user.roleLabel).to.equal('Moderator') | 494 | expect(user.roleLabel).to.equal('Moderator') |
@@ -465,8 +501,20 @@ describe('Test users', function () { | |||
465 | accessTokenUser = await userLogin(server, user) | 501 | accessTokenUser = await userLogin(server, user) |
466 | }) | 502 | }) |
467 | 503 | ||
468 | it('Should not be able to delete a user by a moderator', async function () { | 504 | it('Should be able to update another user password', async function () { |
469 | await removeUser(server.url, 2, accessTokenUser, 403) | 505 | await updateUser({ |
506 | url: server.url, | ||
507 | userId, | ||
508 | accessToken, | ||
509 | password: 'password updated' | ||
510 | }) | ||
511 | |||
512 | await getMyUserVideoQuotaUsed(server.url, accessTokenUser, 401) | ||
513 | |||
514 | await userLogin(server, user, 400) | ||
515 | |||
516 | user.password = 'password updated' | ||
517 | accessTokenUser = await userLogin(server, user) | ||
470 | }) | 518 | }) |
471 | 519 | ||
472 | it('Should be able to list video blacklist by a moderator', async function () { | 520 | it('Should be able to list video blacklist by a moderator', async function () { |
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 09bb62a8d..a501a80b2 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -3,16 +3,17 @@ import './services' | |||
3 | import './single-server' | 3 | import './single-server' |
4 | import './video-abuse' | 4 | import './video-abuse' |
5 | import './video-blacklist' | 5 | import './video-blacklist' |
6 | import './video-blacklist-management' | ||
7 | import './video-captions' | 6 | import './video-captions' |
8 | import './video-change-ownership' | 7 | import './video-change-ownership' |
9 | import './video-channels' | 8 | import './video-channels' |
10 | import './video-comments' | 9 | import './video-comments' |
11 | import './video-description' | 10 | import './video-description' |
11 | import './video-hls' | ||
12 | import './video-imports' | 12 | import './video-imports' |
13 | import './video-nsfw' | 13 | import './video-nsfw' |
14 | import './video-privacy' | 14 | import './video-privacy' |
15 | import './video-schedule-update' | 15 | import './video-schedule-update' |
16 | import './video-transcoder' | 16 | import './video-transcoder' |
17 | import './videos-filter' | ||
17 | import './videos-history' | 18 | import './videos-history' |
18 | import './videos-overview' | 19 | import './videos-overview' |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 99b74ccff..1b471ba79 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -8,6 +8,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos' | |||
8 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | 8 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' |
9 | import { | 9 | import { |
10 | addVideoChannel, | 10 | addVideoChannel, |
11 | checkTmpIsEmpty, | ||
11 | checkVideoFilesWereRemoved, | 12 | checkVideoFilesWereRemoved, |
12 | completeVideoCheck, | 13 | completeVideoCheck, |
13 | createUser, | 14 | createUser, |
@@ -31,15 +32,15 @@ import { | |||
31 | viewVideo, | 32 | viewVideo, |
32 | wait, | 33 | wait, |
33 | webtorrentAdd | 34 | webtorrentAdd |
34 | } from '../../utils' | 35 | } from '../../../../shared/utils' |
35 | import { | 36 | import { |
36 | addVideoCommentReply, | 37 | addVideoCommentReply, |
37 | addVideoCommentThread, | 38 | addVideoCommentThread, |
38 | deleteVideoComment, | 39 | deleteVideoComment, |
39 | getVideoCommentThreads, | 40 | getVideoCommentThreads, |
40 | getVideoThreadComments | 41 | getVideoThreadComments |
41 | } from '../../utils/videos/video-comments' | 42 | } from '../../../../shared/utils/videos/video-comments' |
42 | import { waitJobs } from '../../utils/server/jobs' | 43 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
43 | 44 | ||
44 | const expect = chai.expect | 45 | const expect = chai.expect |
45 | 46 | ||
@@ -995,19 +996,19 @@ describe('Test multiple servers', function () { | |||
995 | files: [ | 996 | files: [ |
996 | { | 997 | { |
997 | resolution: 720, | 998 | resolution: 720, |
998 | size: 36000 | 999 | size: 72000 |
999 | }, | 1000 | }, |
1000 | { | 1001 | { |
1001 | resolution: 480, | 1002 | resolution: 480, |
1002 | size: 21000 | 1003 | size: 45000 |
1003 | }, | 1004 | }, |
1004 | { | 1005 | { |
1005 | resolution: 360, | 1006 | resolution: 360, |
1006 | size: 17000 | 1007 | size: 34600 |
1007 | }, | 1008 | }, |
1008 | { | 1009 | { |
1009 | resolution: 240, | 1010 | resolution: 240, |
1010 | size: 13000 | 1011 | size: 24770 |
1011 | } | 1012 | } |
1012 | ] | 1013 | ] |
1013 | } | 1014 | } |
@@ -1016,6 +1017,14 @@ describe('Test multiple servers', function () { | |||
1016 | }) | 1017 | }) |
1017 | }) | 1018 | }) |
1018 | 1019 | ||
1020 | describe('TMP directory', function () { | ||
1021 | it('Should have an empty tmp directory', async function () { | ||
1022 | for (const server of servers) { | ||
1023 | await checkTmpIsEmpty(server) | ||
1024 | } | ||
1025 | }) | ||
1026 | }) | ||
1027 | |||
1019 | after(async function () { | 1028 | after(async function () { |
1020 | killallServers(servers) | 1029 | killallServers(servers) |
1021 | 1030 | ||
diff --git a/server/tests/api/videos/services.ts b/server/tests/api/videos/services.ts index 2f1424292..2da86964f 100644 --- a/server/tests/api/videos/services.ts +++ b/server/tests/api/videos/services.ts | |||
@@ -2,8 +2,16 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { flushTests, getOEmbed, getVideosList, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index' | 5 | import { |
6 | import { runServer } from '../../utils/server/servers' | 6 | flushTests, |
7 | getOEmbed, | ||
8 | getVideosList, | ||
9 | killallServers, | ||
10 | ServerInfo, | ||
11 | setAccessTokensToServers, | ||
12 | uploadVideo | ||
13 | } from '../../../../shared/utils/index' | ||
14 | import { runServer } from '../../../../shared/utils/server/servers' | ||
7 | 15 | ||
8 | const expect = chai.expect | 16 | const expect = chai.expect |
9 | 17 | ||
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index 92d42eb80..cfdcbaf3f 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -28,7 +28,7 @@ import { | |||
28 | uploadVideo, | 28 | uploadVideo, |
29 | viewVideo, | 29 | viewVideo, |
30 | wait | 30 | wait |
31 | } from '../../utils' | 31 | } from '../../../../shared/utils' |
32 | 32 | ||
33 | const expect = chai.expect | 33 | const expect = chai.expect |
34 | 34 | ||
@@ -120,7 +120,7 @@ describe('Test a single server', function () { | |||
120 | const categories = res.body | 120 | const categories = res.body |
121 | expect(Object.keys(categories)).to.have.length.above(10) | 121 | expect(Object.keys(categories)).to.have.length.above(10) |
122 | 122 | ||
123 | expect(categories[11]).to.equal('News') | 123 | expect(categories[11]).to.equal('News & Politics') |
124 | }) | 124 | }) |
125 | 125 | ||
126 | it('Should list video licences', async function () { | 126 | it('Should list video licences', async function () { |
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts index a17f3c8de..3a7b623da 100644 --- a/server/tests/api/videos/video-abuse.ts +++ b/server/tests/api/videos/video-abuse.ts | |||
@@ -14,9 +14,9 @@ import { | |||
14 | setAccessTokensToServers, | 14 | setAccessTokensToServers, |
15 | updateVideoAbuse, | 15 | updateVideoAbuse, |
16 | uploadVideo | 16 | uploadVideo |
17 | } from '../../utils/index' | 17 | } from '../../../../shared/utils/index' |
18 | import { doubleFollow } from '../../utils/server/follows' | 18 | import { doubleFollow } from '../../../../shared/utils/server/follows' |
19 | import { waitJobs } from '../../utils/server/jobs' | 19 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
20 | 20 | ||
21 | const expect = chai.expect | 21 | const expect = chai.expect |
22 | 22 | ||
diff --git a/server/tests/api/videos/video-blacklist-management.ts b/server/tests/api/videos/video-blacklist-management.ts deleted file mode 100644 index 7bf39dc99..000000000 --- a/server/tests/api/videos/video-blacklist-management.ts +++ /dev/null | |||
@@ -1,193 +0,0 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import * as lodash from 'lodash' | ||
5 | import 'mocha' | ||
6 | import { | ||
7 | addVideoToBlacklist, | ||
8 | flushAndRunMultipleServers, | ||
9 | getBlacklistedVideosList, | ||
10 | getMyVideos, | ||
11 | getSortedBlacklistedVideosList, | ||
12 | getVideosList, | ||
13 | killallServers, | ||
14 | removeVideoFromBlacklist, | ||
15 | ServerInfo, | ||
16 | setAccessTokensToServers, | ||
17 | updateVideoBlacklist, | ||
18 | uploadVideo | ||
19 | } from '../../utils/index' | ||
20 | import { doubleFollow } from '../../utils/server/follows' | ||
21 | import { waitJobs } from '../../utils/server/jobs' | ||
22 | import { VideoAbuse } from '../../../../shared/models/videos' | ||
23 | |||
24 | const expect = chai.expect | ||
25 | const orderBy = lodash.orderBy | ||
26 | |||
27 | describe('Test video blacklist management', function () { | ||
28 | let servers: ServerInfo[] = [] | ||
29 | let videoId: number | ||
30 | |||
31 | async function blacklistVideosOnServer (server: ServerInfo) { | ||
32 | const res = await getVideosList(server.url) | ||
33 | |||
34 | const videos = res.body.data | ||
35 | for (let video of videos) { | ||
36 | await addVideoToBlacklist(server.url, server.accessToken, video.id, 'super reason') | ||
37 | } | ||
38 | } | ||
39 | |||
40 | before(async function () { | ||
41 | this.timeout(50000) | ||
42 | |||
43 | // Run servers | ||
44 | servers = await flushAndRunMultipleServers(2) | ||
45 | |||
46 | // Get the access tokens | ||
47 | await setAccessTokensToServers(servers) | ||
48 | |||
49 | // Server 1 and server 2 follow each other | ||
50 | await doubleFollow(servers[0], servers[1]) | ||
51 | |||
52 | // Upload 2 videos on server 2 | ||
53 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 1st video', description: 'A video on server 2' }) | ||
54 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 2nd video', description: 'A video on server 2' }) | ||
55 | |||
56 | // Wait videos propagation, server 2 has transcoding enabled | ||
57 | await waitJobs(servers) | ||
58 | |||
59 | // Blacklist the two videos on server 1 | ||
60 | await blacklistVideosOnServer(servers[0]) | ||
61 | }) | ||
62 | |||
63 | describe('When listing blacklisted videos', function () { | ||
64 | it('Should display all the blacklisted videos', async function () { | ||
65 | const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken) | ||
66 | |||
67 | expect(res.body.total).to.equal(2) | ||
68 | |||
69 | const blacklistedVideos = res.body.data | ||
70 | expect(blacklistedVideos).to.be.an('array') | ||
71 | expect(blacklistedVideos.length).to.equal(2) | ||
72 | |||
73 | for (const blacklistedVideo of blacklistedVideos) { | ||
74 | expect(blacklistedVideo.reason).to.equal('super reason') | ||
75 | videoId = blacklistedVideo.video.id | ||
76 | } | ||
77 | }) | ||
78 | |||
79 | it('Should get the correct sort when sorting by descending id', async function () { | ||
80 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id') | ||
81 | expect(res.body.total).to.equal(2) | ||
82 | |||
83 | const blacklistedVideos = res.body.data | ||
84 | expect(blacklistedVideos).to.be.an('array') | ||
85 | expect(blacklistedVideos.length).to.equal(2) | ||
86 | |||
87 | const result = orderBy(res.body.data, [ 'id' ], [ 'desc' ]) | ||
88 | |||
89 | expect(blacklistedVideos).to.deep.equal(result) | ||
90 | }) | ||
91 | |||
92 | it('Should get the correct sort when sorting by descending video name', async function () { | ||
93 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
94 | expect(res.body.total).to.equal(2) | ||
95 | |||
96 | const blacklistedVideos = res.body.data | ||
97 | expect(blacklistedVideos).to.be.an('array') | ||
98 | expect(blacklistedVideos.length).to.equal(2) | ||
99 | |||
100 | const result = orderBy(res.body.data, [ 'name' ], [ 'desc' ]) | ||
101 | |||
102 | expect(blacklistedVideos).to.deep.equal(result) | ||
103 | }) | ||
104 | |||
105 | it('Should get the correct sort when sorting by ascending creation date', async function () { | ||
106 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt') | ||
107 | expect(res.body.total).to.equal(2) | ||
108 | |||
109 | const blacklistedVideos = res.body.data | ||
110 | expect(blacklistedVideos).to.be.an('array') | ||
111 | expect(blacklistedVideos.length).to.equal(2) | ||
112 | |||
113 | const result = orderBy(res.body.data, [ 'createdAt' ]) | ||
114 | |||
115 | expect(blacklistedVideos).to.deep.equal(result) | ||
116 | }) | ||
117 | }) | ||
118 | |||
119 | describe('When updating blacklisted videos', function () { | ||
120 | it('Should change the reason', async function () { | ||
121 | await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated') | ||
122 | |||
123 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
124 | const video = res.body.data.find(b => b.video.id === videoId) | ||
125 | |||
126 | expect(video.reason).to.equal('my super reason updated') | ||
127 | }) | ||
128 | }) | ||
129 | |||
130 | describe('When listing my videos', function () { | ||
131 | it('Should display blacklisted videos', async function () { | ||
132 | await blacklistVideosOnServer(servers[1]) | ||
133 | |||
134 | const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 5) | ||
135 | |||
136 | expect(res.body.total).to.equal(2) | ||
137 | expect(res.body.data).to.have.lengthOf(2) | ||
138 | |||
139 | for (const video of res.body.data) { | ||
140 | expect(video.blacklisted).to.be.true | ||
141 | expect(video.blacklistedReason).to.equal('super reason') | ||
142 | } | ||
143 | }) | ||
144 | }) | ||
145 | |||
146 | describe('When removing a blacklisted video', function () { | ||
147 | let videoToRemove: VideoAbuse | ||
148 | let blacklist = [] | ||
149 | |||
150 | it('Should not have any video in videos list on server 1', async function () { | ||
151 | const res = await getVideosList(servers[0].url) | ||
152 | expect(res.body.total).to.equal(0) | ||
153 | expect(res.body.data).to.be.an('array') | ||
154 | expect(res.body.data.length).to.equal(0) | ||
155 | }) | ||
156 | |||
157 | it('Should remove a video from the blacklist on server 1', async function () { | ||
158 | // Get one video in the blacklist | ||
159 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
160 | videoToRemove = res.body.data[0] | ||
161 | blacklist = res.body.data.slice(1) | ||
162 | |||
163 | // Remove it | ||
164 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoToRemove.video.id) | ||
165 | }) | ||
166 | |||
167 | it('Should have the ex-blacklisted video in videos list on server 1', async function () { | ||
168 | const res = await getVideosList(servers[0].url) | ||
169 | expect(res.body.total).to.equal(1) | ||
170 | |||
171 | const videos = res.body.data | ||
172 | expect(videos).to.be.an('array') | ||
173 | expect(videos.length).to.equal(1) | ||
174 | |||
175 | expect(videos[0].name).to.equal(videoToRemove.video.name) | ||
176 | expect(videos[0].id).to.equal(videoToRemove.video.id) | ||
177 | }) | ||
178 | |||
179 | it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () { | ||
180 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
181 | expect(res.body.total).to.equal(1) | ||
182 | |||
183 | const videos = res.body.data | ||
184 | expect(videos).to.be.an('array') | ||
185 | expect(videos.length).to.equal(1) | ||
186 | expect(videos).to.deep.equal(blacklist) | ||
187 | }) | ||
188 | }) | ||
189 | |||
190 | after(async function () { | ||
191 | killallServers(servers) | ||
192 | }) | ||
193 | }) | ||
diff --git a/server/tests/api/videos/video-blacklist.ts b/server/tests/api/videos/video-blacklist.ts index de4c68f1d..d39ad63b4 100644 --- a/server/tests/api/videos/video-blacklist.ts +++ b/server/tests/api/videos/video-blacklist.ts | |||
@@ -1,24 +1,43 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | 1 | /* tslint:disable:no-unused-expression */ |
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import { orderBy } from 'lodash' | ||
4 | import 'mocha' | 5 | import 'mocha' |
5 | import { | 6 | import { |
6 | addVideoToBlacklist, | 7 | addVideoToBlacklist, |
7 | flushAndRunMultipleServers, | 8 | flushAndRunMultipleServers, |
9 | getBlacklistedVideosList, | ||
10 | getMyVideos, | ||
11 | getSortedBlacklistedVideosList, | ||
8 | getVideosList, | 12 | getVideosList, |
9 | killallServers, | 13 | killallServers, |
14 | removeVideoFromBlacklist, | ||
10 | searchVideo, | 15 | searchVideo, |
11 | ServerInfo, | 16 | ServerInfo, |
12 | setAccessTokensToServers, | 17 | setAccessTokensToServers, |
13 | uploadVideo | 18 | updateVideo, |
14 | } from '../../utils/index' | 19 | updateVideoBlacklist, |
15 | import { doubleFollow } from '../../utils/server/follows' | 20 | uploadVideo, |
16 | import { waitJobs } from '../../utils/server/jobs' | 21 | viewVideo |
22 | } from '../../../../shared/utils/index' | ||
23 | import { doubleFollow } from '../../../../shared/utils/server/follows' | ||
24 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
25 | import { VideoBlacklist } from '../../../../shared/models/videos' | ||
17 | 26 | ||
18 | const expect = chai.expect | 27 | const expect = chai.expect |
19 | 28 | ||
20 | describe('Test video blacklists', function () { | 29 | describe('Test video blacklist management', function () { |
21 | let servers: ServerInfo[] = [] | 30 | let servers: ServerInfo[] = [] |
31 | let videoId: number | ||
32 | |||
33 | async function blacklistVideosOnServer (server: ServerInfo) { | ||
34 | const res = await getVideosList(server.url) | ||
35 | |||
36 | const videos = res.body.data | ||
37 | for (let video of videos) { | ||
38 | await addVideoToBlacklist(server.url, server.accessToken, video.id, 'super reason') | ||
39 | } | ||
40 | } | ||
22 | 41 | ||
23 | before(async function () { | 42 | before(async function () { |
24 | this.timeout(50000) | 43 | this.timeout(50000) |
@@ -32,58 +51,270 @@ describe('Test video blacklists', function () { | |||
32 | // Server 1 and server 2 follow each other | 51 | // Server 1 and server 2 follow each other |
33 | await doubleFollow(servers[0], servers[1]) | 52 | await doubleFollow(servers[0], servers[1]) |
34 | 53 | ||
35 | // Upload a video on server 2 | 54 | // Upload 2 videos on server 2 |
36 | const videoAttributes = { | 55 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 1st video', description: 'A video on server 2' }) |
37 | name: 'my super name for server 2', | 56 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 2nd video', description: 'A video on server 2' }) |
38 | description: 'my super description for server 2' | ||
39 | } | ||
40 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) | ||
41 | 57 | ||
42 | // Wait videos propagation, server 2 has transcoding enabled | 58 | // Wait videos propagation, server 2 has transcoding enabled |
43 | await waitJobs(servers) | 59 | await waitJobs(servers) |
44 | 60 | ||
45 | const res = await getVideosList(servers[0].url) | 61 | // Blacklist the two videos on server 1 |
46 | const videos = res.body.data | 62 | await blacklistVideosOnServer(servers[0]) |
63 | }) | ||
64 | |||
65 | describe('When listing/searching videos', function () { | ||
47 | 66 | ||
48 | expect(videos.length).to.equal(1) | 67 | it('Should not have the video blacklisted in videos list/search on server 1', async function () { |
68 | { | ||
69 | const res = await getVideosList(servers[ 0 ].url) | ||
49 | 70 | ||
50 | servers[0].remoteVideo = videos.find(video => video.name === 'my super name for server 2') | 71 | expect(res.body.total).to.equal(0) |
72 | expect(res.body.data).to.be.an('array') | ||
73 | expect(res.body.data.length).to.equal(0) | ||
74 | } | ||
75 | |||
76 | { | ||
77 | const res = await searchVideo(servers[ 0 ].url, 'name') | ||
78 | |||
79 | expect(res.body.total).to.equal(0) | ||
80 | expect(res.body.data).to.be.an('array') | ||
81 | expect(res.body.data.length).to.equal(0) | ||
82 | } | ||
83 | }) | ||
84 | |||
85 | it('Should have the blacklisted video in videos list/search on server 2', async function () { | ||
86 | { | ||
87 | const res = await getVideosList(servers[ 1 ].url) | ||
88 | |||
89 | expect(res.body.total).to.equal(2) | ||
90 | expect(res.body.data).to.be.an('array') | ||
91 | expect(res.body.data.length).to.equal(2) | ||
92 | } | ||
93 | |||
94 | { | ||
95 | const res = await searchVideo(servers[ 1 ].url, 'video') | ||
96 | |||
97 | expect(res.body.total).to.equal(2) | ||
98 | expect(res.body.data).to.be.an('array') | ||
99 | expect(res.body.data.length).to.equal(2) | ||
100 | } | ||
101 | }) | ||
51 | }) | 102 | }) |
52 | 103 | ||
53 | it('Should blacklist a remote video on server 1', async function () { | 104 | describe('When listing blacklisted videos', function () { |
54 | await addVideoToBlacklist(servers[0].url, servers[0].accessToken, servers[0].remoteVideo.id) | 105 | it('Should display all the blacklisted videos', async function () { |
106 | const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken) | ||
107 | |||
108 | expect(res.body.total).to.equal(2) | ||
109 | |||
110 | const blacklistedVideos = res.body.data | ||
111 | expect(blacklistedVideos).to.be.an('array') | ||
112 | expect(blacklistedVideos.length).to.equal(2) | ||
113 | |||
114 | for (const blacklistedVideo of blacklistedVideos) { | ||
115 | expect(blacklistedVideo.reason).to.equal('super reason') | ||
116 | videoId = blacklistedVideo.video.id | ||
117 | } | ||
118 | }) | ||
119 | |||
120 | it('Should get the correct sort when sorting by descending id', async function () { | ||
121 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id') | ||
122 | expect(res.body.total).to.equal(2) | ||
123 | |||
124 | const blacklistedVideos = res.body.data | ||
125 | expect(blacklistedVideos).to.be.an('array') | ||
126 | expect(blacklistedVideos.length).to.equal(2) | ||
127 | |||
128 | const result = orderBy(res.body.data, [ 'id' ], [ 'desc' ]) | ||
129 | |||
130 | expect(blacklistedVideos).to.deep.equal(result) | ||
131 | }) | ||
132 | |||
133 | it('Should get the correct sort when sorting by descending video name', async function () { | ||
134 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
135 | expect(res.body.total).to.equal(2) | ||
136 | |||
137 | const blacklistedVideos = res.body.data | ||
138 | expect(blacklistedVideos).to.be.an('array') | ||
139 | expect(blacklistedVideos.length).to.equal(2) | ||
140 | |||
141 | const result = orderBy(res.body.data, [ 'name' ], [ 'desc' ]) | ||
142 | |||
143 | expect(blacklistedVideos).to.deep.equal(result) | ||
144 | }) | ||
145 | |||
146 | it('Should get the correct sort when sorting by ascending creation date', async function () { | ||
147 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt') | ||
148 | expect(res.body.total).to.equal(2) | ||
149 | |||
150 | const blacklistedVideos = res.body.data | ||
151 | expect(blacklistedVideos).to.be.an('array') | ||
152 | expect(blacklistedVideos.length).to.equal(2) | ||
153 | |||
154 | const result = orderBy(res.body.data, [ 'createdAt' ]) | ||
155 | |||
156 | expect(blacklistedVideos).to.deep.equal(result) | ||
157 | }) | ||
55 | }) | 158 | }) |
56 | 159 | ||
57 | it('Should not have the video blacklisted in videos list on server 1', async function () { | 160 | describe('When updating blacklisted videos', function () { |
58 | const res = await getVideosList(servers[0].url) | 161 | it('Should change the reason', async function () { |
162 | await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated') | ||
163 | |||
164 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
165 | const video = res.body.data.find(b => b.video.id === videoId) | ||
59 | 166 | ||
60 | expect(res.body.total).to.equal(0) | 167 | expect(video.reason).to.equal('my super reason updated') |
61 | expect(res.body.data).to.be.an('array') | 168 | }) |
62 | expect(res.body.data.length).to.equal(0) | ||
63 | }) | 169 | }) |
64 | 170 | ||
65 | it('Should not have the video blacklisted in videos search on server 1', async function () { | 171 | describe('When listing my videos', function () { |
66 | const res = await searchVideo(servers[0].url, 'name') | 172 | it('Should display blacklisted videos', async function () { |
173 | await blacklistVideosOnServer(servers[1]) | ||
174 | |||
175 | const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 5) | ||
67 | 176 | ||
68 | expect(res.body.total).to.equal(0) | 177 | expect(res.body.total).to.equal(2) |
69 | expect(res.body.data).to.be.an('array') | 178 | expect(res.body.data).to.have.lengthOf(2) |
70 | expect(res.body.data.length).to.equal(0) | 179 | |
180 | for (const video of res.body.data) { | ||
181 | expect(video.blacklisted).to.be.true | ||
182 | expect(video.blacklistedReason).to.equal('super reason') | ||
183 | } | ||
184 | }) | ||
71 | }) | 185 | }) |
72 | 186 | ||
73 | it('Should have the blacklisted video in videos list on server 2', async function () { | 187 | describe('When removing a blacklisted video', function () { |
74 | const res = await getVideosList(servers[1].url) | 188 | let videoToRemove: VideoBlacklist |
189 | let blacklist = [] | ||
190 | |||
191 | it('Should not have any video in videos list on server 1', async function () { | ||
192 | const res = await getVideosList(servers[0].url) | ||
193 | expect(res.body.total).to.equal(0) | ||
194 | expect(res.body.data).to.be.an('array') | ||
195 | expect(res.body.data.length).to.equal(0) | ||
196 | }) | ||
197 | |||
198 | it('Should remove a video from the blacklist on server 1', async function () { | ||
199 | // Get one video in the blacklist | ||
200 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
201 | videoToRemove = res.body.data[0] | ||
202 | blacklist = res.body.data.slice(1) | ||
203 | |||
204 | // Remove it | ||
205 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoToRemove.video.id) | ||
206 | }) | ||
207 | |||
208 | it('Should have the ex-blacklisted video in videos list on server 1', async function () { | ||
209 | const res = await getVideosList(servers[0].url) | ||
210 | expect(res.body.total).to.equal(1) | ||
211 | |||
212 | const videos = res.body.data | ||
213 | expect(videos).to.be.an('array') | ||
214 | expect(videos.length).to.equal(1) | ||
215 | |||
216 | expect(videos[0].name).to.equal(videoToRemove.video.name) | ||
217 | expect(videos[0].id).to.equal(videoToRemove.video.id) | ||
218 | }) | ||
219 | |||
220 | it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () { | ||
221 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
222 | expect(res.body.total).to.equal(1) | ||
75 | 223 | ||
76 | expect(res.body.total).to.equal(1) | 224 | const videos = res.body.data |
77 | expect(res.body.data).to.be.an('array') | 225 | expect(videos).to.be.an('array') |
78 | expect(res.body.data.length).to.equal(1) | 226 | expect(videos.length).to.equal(1) |
227 | expect(videos).to.deep.equal(blacklist) | ||
228 | }) | ||
79 | }) | 229 | }) |
80 | 230 | ||
81 | it('Should have the video blacklisted in videos search on server 2', async function () { | 231 | describe('When blacklisting local videos', function () { |
82 | const res = await searchVideo(servers[1].url, 'name') | 232 | let video3UUID: string |
233 | let video4UUID: string | ||
234 | |||
235 | before(async function () { | ||
236 | this.timeout(10000) | ||
237 | |||
238 | { | ||
239 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'Video 3' }) | ||
240 | video3UUID = res.body.video.uuid | ||
241 | } | ||
242 | { | ||
243 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'Video 4' }) | ||
244 | video4UUID = res.body.video.uuid | ||
245 | } | ||
246 | |||
247 | await waitJobs(servers) | ||
248 | }) | ||
249 | |||
250 | it('Should blacklist video 3 and keep it federated', async function () { | ||
251 | this.timeout(10000) | ||
252 | |||
253 | await addVideoToBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video3UUID, 'super reason', false) | ||
254 | |||
255 | await waitJobs(servers) | ||
256 | |||
257 | { | ||
258 | const res = await getVideosList(servers[ 0 ].url) | ||
259 | expect(res.body.data.find(v => v.uuid === video3UUID)).to.be.undefined | ||
260 | } | ||
261 | |||
262 | { | ||
263 | const res = await getVideosList(servers[ 1 ].url) | ||
264 | expect(res.body.data.find(v => v.uuid === video3UUID)).to.not.be.undefined | ||
265 | } | ||
266 | }) | ||
267 | |||
268 | it('Should unfederate the video', async function () { | ||
269 | this.timeout(10000) | ||
270 | |||
271 | await addVideoToBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video4UUID, 'super reason', true) | ||
272 | |||
273 | await waitJobs(servers) | ||
274 | |||
275 | for (const server of servers) { | ||
276 | const res = await getVideosList(server.url) | ||
277 | expect(res.body.data.find(v => v.uuid === video4UUID)).to.be.undefined | ||
278 | } | ||
279 | }) | ||
280 | |||
281 | it('Should have the video unfederated even after an Update AP message', async function () { | ||
282 | this.timeout(10000) | ||
283 | |||
284 | await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, video4UUID, { description: 'super description' }) | ||
285 | |||
286 | await waitJobs(servers) | ||
287 | |||
288 | for (const server of servers) { | ||
289 | const res = await getVideosList(server.url) | ||
290 | expect(res.body.data.find(v => v.uuid === video4UUID)).to.be.undefined | ||
291 | } | ||
292 | }) | ||
293 | |||
294 | it('Should have the correct video blacklist unfederate attribute', async function () { | ||
295 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt') | ||
296 | |||
297 | const blacklistedVideos: VideoBlacklist[] = res.body.data | ||
298 | const video3Blacklisted = blacklistedVideos.find(b => b.video.uuid === video3UUID) | ||
299 | const video4Blacklisted = blacklistedVideos.find(b => b.video.uuid === video4UUID) | ||
300 | |||
301 | expect(video3Blacklisted.unfederated).to.be.false | ||
302 | expect(video4Blacklisted.unfederated).to.be.true | ||
303 | }) | ||
304 | |||
305 | it('Should remove the video from blacklist and refederate the video', async function () { | ||
306 | this.timeout(10000) | ||
307 | |||
308 | await removeVideoFromBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video4UUID) | ||
309 | |||
310 | await waitJobs(servers) | ||
311 | |||
312 | for (const server of servers) { | ||
313 | const res = await getVideosList(server.url) | ||
314 | expect(res.body.data.find(v => v.uuid === video4UUID)).to.not.be.undefined | ||
315 | } | ||
316 | }) | ||
83 | 317 | ||
84 | expect(res.body.total).to.equal(1) | ||
85 | expect(res.body.data).to.be.an('array') | ||
86 | expect(res.body.data.length).to.equal(1) | ||
87 | }) | 318 | }) |
88 | 319 | ||
89 | after(async function () { | 320 | after(async function () { |
diff --git a/server/tests/api/videos/video-captions.ts b/server/tests/api/videos/video-captions.ts index 6e441410d..57bee713f 100644 --- a/server/tests/api/videos/video-captions.ts +++ b/server/tests/api/videos/video-captions.ts | |||
@@ -2,10 +2,17 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { checkVideoFilesWereRemoved, doubleFollow, flushAndRunMultipleServers, removeVideo, uploadVideo, wait } from '../../utils' | 5 | import { |
6 | import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' | 6 | checkVideoFilesWereRemoved, |
7 | import { waitJobs } from '../../utils/server/jobs' | 7 | doubleFollow, |
8 | import { createVideoCaption, deleteVideoCaption, listVideoCaptions, testCaptionFile } from '../../utils/videos/video-captions' | 8 | flushAndRunMultipleServers, |
9 | removeVideo, | ||
10 | uploadVideo, | ||
11 | wait | ||
12 | } from '../../../../shared/utils' | ||
13 | import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index' | ||
14 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
15 | import { createVideoCaption, deleteVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/utils/videos/video-captions' | ||
9 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' | 16 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' |
10 | 17 | ||
11 | const expect = chai.expect | 18 | const expect = chai.expect |
diff --git a/server/tests/api/videos/video-change-ownership.ts b/server/tests/api/videos/video-change-ownership.ts index 1578a471d..25675a966 100644 --- a/server/tests/api/videos/video-change-ownership.ts +++ b/server/tests/api/videos/video-change-ownership.ts | |||
@@ -18,8 +18,8 @@ import { | |||
18 | uploadVideo, | 18 | uploadVideo, |
19 | userLogin, | 19 | userLogin, |
20 | getVideo | 20 | getVideo |
21 | } from '../../utils' | 21 | } from '../../../../shared/utils' |
22 | import { waitJobs } from '../../utils/server/jobs' | 22 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
23 | import { User } from '../../../../shared/models/users' | 23 | import { User } from '../../../../shared/models/users' |
24 | import { VideoDetails } from '../../../../shared/models/videos' | 24 | import { VideoDetails } from '../../../../shared/models/videos' |
25 | 25 | ||
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index 8138c65d6..63514d69c 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts | |||
@@ -7,11 +7,13 @@ import { | |||
7 | createUser, | 7 | createUser, |
8 | doubleFollow, | 8 | doubleFollow, |
9 | flushAndRunMultipleServers, | 9 | flushAndRunMultipleServers, |
10 | getVideoChannelVideos, serverLogin, testImage, | 10 | getVideoChannelVideos, |
11 | testImage, | ||
11 | updateVideo, | 12 | updateVideo, |
12 | updateVideoChannelAvatar, | 13 | updateVideoChannelAvatar, |
13 | uploadVideo, wait, userLogin | 14 | uploadVideo, |
14 | } from '../../utils' | 15 | userLogin |
16 | } from '../../../../shared/utils' | ||
15 | import { | 17 | import { |
16 | addVideoChannel, | 18 | addVideoChannel, |
17 | deleteVideoChannel, | 19 | deleteVideoChannel, |
@@ -24,8 +26,8 @@ import { | |||
24 | ServerInfo, | 26 | ServerInfo, |
25 | setAccessTokensToServers, | 27 | setAccessTokensToServers, |
26 | updateVideoChannel | 28 | updateVideoChannel |
27 | } from '../../utils/index' | 29 | } from '../../../../shared/utils/index' |
28 | import { waitJobs } from '../../utils/server/jobs' | 30 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
29 | 31 | ||
30 | const expect = chai.expect | 32 | const expect = chai.expect |
31 | 33 | ||
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts index d6e07c5b3..ce1b17e35 100644 --- a/server/tests/api/videos/video-comments.ts +++ b/server/tests/api/videos/video-comments.ts | |||
@@ -3,7 +3,7 @@ | |||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | 5 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' |
6 | import { testImage } from '../../utils' | 6 | import { testImage } from '../../../../shared/utils' |
7 | import { | 7 | import { |
8 | dateIsValid, | 8 | dateIsValid, |
9 | flushTests, | 9 | flushTests, |
@@ -13,14 +13,14 @@ import { | |||
13 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | updateMyAvatar, | 14 | updateMyAvatar, |
15 | uploadVideo | 15 | uploadVideo |
16 | } from '../../utils/index' | 16 | } from '../../../../shared/utils/index' |
17 | import { | 17 | import { |
18 | addVideoCommentReply, | 18 | addVideoCommentReply, |
19 | addVideoCommentThread, | 19 | addVideoCommentThread, |
20 | deleteVideoComment, | 20 | deleteVideoComment, |
21 | getVideoCommentThreads, | 21 | getVideoCommentThreads, |
22 | getVideoThreadComments | 22 | getVideoThreadComments |
23 | } from '../../utils/videos/video-comments' | 23 | } from '../../../../shared/utils/videos/video-comments' |
24 | 24 | ||
25 | const expect = chai.expect | 25 | const expect = chai.expect |
26 | 26 | ||
diff --git a/server/tests/api/videos/video-description.ts b/server/tests/api/videos/video-description.ts index dd5cd78c0..cbda0b9a6 100644 --- a/server/tests/api/videos/video-description.ts +++ b/server/tests/api/videos/video-description.ts | |||
@@ -12,9 +12,9 @@ import { | |||
12 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | updateVideo, | 13 | updateVideo, |
14 | uploadVideo | 14 | uploadVideo |
15 | } from '../../utils/index' | 15 | } from '../../../../shared/utils/index' |
16 | import { doubleFollow } from '../../utils/server/follows' | 16 | import { doubleFollow } from '../../../../shared/utils/server/follows' |
17 | import { waitJobs } from '../../utils/server/jobs' | 17 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
18 | 18 | ||
19 | const expect = chai.expect | 19 | const expect = chai.expect |
20 | 20 | ||
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts new file mode 100644 index 000000000..a1214bad1 --- /dev/null +++ b/server/tests/api/videos/video-hls.ts | |||
@@ -0,0 +1,139 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | checkDirectoryIsEmpty, | ||
7 | checkSegmentHash, | ||
8 | checkTmpIsEmpty, | ||
9 | doubleFollow, | ||
10 | flushAndRunMultipleServers, | ||
11 | flushTests, | ||
12 | getPlaylist, | ||
13 | getVideo, | ||
14 | killallServers, | ||
15 | removeVideo, | ||
16 | ServerInfo, | ||
17 | setAccessTokensToServers, | ||
18 | updateVideo, | ||
19 | uploadVideo, | ||
20 | waitJobs | ||
21 | } from '../../../../shared/utils' | ||
22 | import { VideoDetails } from '../../../../shared/models/videos' | ||
23 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | ||
24 | import { join } from 'path' | ||
25 | |||
26 | const expect = chai.expect | ||
27 | |||
28 | async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) { | ||
29 | const resolutions = [ 240, 360, 480, 720 ] | ||
30 | |||
31 | for (const server of servers) { | ||
32 | const res = await getVideo(server.url, videoUUID) | ||
33 | const videoDetails: VideoDetails = res.body | ||
34 | |||
35 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
36 | |||
37 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
38 | expect(hlsPlaylist).to.not.be.undefined | ||
39 | |||
40 | { | ||
41 | const res2 = await getPlaylist(hlsPlaylist.playlistUrl) | ||
42 | |||
43 | const masterPlaylist = res2.text | ||
44 | |||
45 | expect(masterPlaylist).to.contain('#EXT-X-STREAM-INF:BANDWIDTH=55472,RESOLUTION=640x360,FRAME-RATE=25') | ||
46 | |||
47 | for (const resolution of resolutions) { | ||
48 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | { | ||
53 | for (const resolution of resolutions) { | ||
54 | const res2 = await getPlaylist(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}.m3u8`) | ||
55 | |||
56 | const subPlaylist = res2.text | ||
57 | expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) | ||
58 | } | ||
59 | } | ||
60 | |||
61 | { | ||
62 | const baseUrl = 'http://localhost:9001/static/playlists/hls' | ||
63 | |||
64 | for (const resolution of resolutions) { | ||
65 | await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist) | ||
66 | } | ||
67 | } | ||
68 | } | ||
69 | } | ||
70 | |||
71 | describe('Test HLS videos', function () { | ||
72 | let servers: ServerInfo[] = [] | ||
73 | let videoUUID = '' | ||
74 | |||
75 | before(async function () { | ||
76 | this.timeout(120000) | ||
77 | |||
78 | servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: true, hls: { enabled: true } } }) | ||
79 | |||
80 | // Get the access tokens | ||
81 | await setAccessTokensToServers(servers) | ||
82 | |||
83 | // Server 1 and server 2 follow each other | ||
84 | await doubleFollow(servers[0], servers[1]) | ||
85 | }) | ||
86 | |||
87 | it('Should upload a video and transcode it to HLS', async function () { | ||
88 | this.timeout(120000) | ||
89 | |||
90 | { | ||
91 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' }) | ||
92 | videoUUID = res.body.video.uuid | ||
93 | } | ||
94 | |||
95 | await waitJobs(servers) | ||
96 | |||
97 | await checkHlsPlaylist(servers, videoUUID) | ||
98 | }) | ||
99 | |||
100 | it('Should update the video', async function () { | ||
101 | await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' }) | ||
102 | |||
103 | await waitJobs(servers) | ||
104 | |||
105 | await checkHlsPlaylist(servers, videoUUID) | ||
106 | }) | ||
107 | |||
108 | it('Should delete the video', async function () { | ||
109 | await removeVideo(servers[0].url, servers[0].accessToken, videoUUID) | ||
110 | |||
111 | await waitJobs(servers) | ||
112 | |||
113 | for (const server of servers) { | ||
114 | await getVideo(server.url, videoUUID, 404) | ||
115 | } | ||
116 | }) | ||
117 | |||
118 | it('Should have the playlists/segment deleted from the disk', async function () { | ||
119 | for (const server of servers) { | ||
120 | await checkDirectoryIsEmpty(server, 'videos') | ||
121 | await checkDirectoryIsEmpty(server, join('playlists', 'hls')) | ||
122 | } | ||
123 | }) | ||
124 | |||
125 | it('Should have an empty tmp directory', async function () { | ||
126 | for (const server of servers) { | ||
127 | await checkTmpIsEmpty(server) | ||
128 | } | ||
129 | }) | ||
130 | |||
131 | after(async function () { | ||
132 | killallServers(servers) | ||
133 | |||
134 | // Keep the logs if the test failed | ||
135 | if (this['ok']) { | ||
136 | await flushTests() | ||
137 | } | ||
138 | }) | ||
139 | }) | ||
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts index b7866d529..cd4988553 100644 --- a/server/tests/api/videos/video-imports.ts +++ b/server/tests/api/videos/video-imports.ts | |||
@@ -14,9 +14,9 @@ import { | |||
14 | killallServers, | 14 | killallServers, |
15 | ServerInfo, | 15 | ServerInfo, |
16 | setAccessTokensToServers | 16 | setAccessTokensToServers |
17 | } from '../../utils' | 17 | } from '../../../../shared/utils' |
18 | import { waitJobs } from '../../utils/server/jobs' | 18 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
19 | import { getMagnetURI, getYoutubeVideoUrl, importVideo, getMyVideoImports } from '../../utils/videos/video-imports' | 19 | import { getMagnetURI, getYoutubeVideoUrl, importVideo, getMyVideoImports } from '../../../../shared/utils/videos/video-imports' |
20 | 20 | ||
21 | const expect = chai.expect | 21 | const expect = chai.expect |
22 | 22 | ||
@@ -30,7 +30,7 @@ describe('Test video imports', function () { | |||
30 | const videoHttp: VideoDetails = resHttp.body | 30 | const videoHttp: VideoDetails = resHttp.body |
31 | 31 | ||
32 | expect(videoHttp.name).to.equal('small video - youtube') | 32 | expect(videoHttp.name).to.equal('small video - youtube') |
33 | expect(videoHttp.category.label).to.equal('News') | 33 | expect(videoHttp.category.label).to.equal('News & Politics') |
34 | expect(videoHttp.licence.label).to.equal('Attribution') | 34 | expect(videoHttp.licence.label).to.equal('Attribution') |
35 | expect(videoHttp.language.label).to.equal('Unknown') | 35 | expect(videoHttp.language.label).to.equal('Unknown') |
36 | expect(videoHttp.nsfw).to.be.false | 36 | expect(videoHttp.nsfw).to.be.false |
diff --git a/server/tests/api/videos/video-nsfw.ts b/server/tests/api/videos/video-nsfw.ts index eab7a6991..df1ee2eb9 100644 --- a/server/tests/api/videos/video-nsfw.ts +++ b/server/tests/api/videos/video-nsfw.ts | |||
@@ -2,10 +2,17 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { flushTests, getVideosList, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index' | 5 | import { |
6 | import { userLogin } from '../../utils/users/login' | 6 | flushTests, |
7 | import { createUser } from '../../utils/users/users' | 7 | getVideosList, |
8 | import { getMyVideos } from '../../utils/videos/videos' | 8 | killallServers, |
9 | ServerInfo, | ||
10 | setAccessTokensToServers, | ||
11 | uploadVideo | ||
12 | } from '../../../../shared/utils/index' | ||
13 | import { userLogin } from '../../../../shared/utils/users/login' | ||
14 | import { createUser } from '../../../../shared/utils/users/users' | ||
15 | import { getMyVideos } from '../../../../shared/utils/videos/videos' | ||
9 | import { | 16 | import { |
10 | getAccountVideos, | 17 | getAccountVideos, |
11 | getConfig, | 18 | getConfig, |
@@ -18,7 +25,7 @@ import { | |||
18 | searchVideoWithToken, | 25 | searchVideoWithToken, |
19 | updateCustomConfig, | 26 | updateCustomConfig, |
20 | updateMyUser | 27 | updateMyUser |
21 | } from '../../utils' | 28 | } from '../../../../shared/utils' |
22 | import { ServerConfig } from '../../../../shared/models' | 29 | import { ServerConfig } from '../../../../shared/models' |
23 | import { CustomConfig } from '../../../../shared/models/server/custom-config.model' | 30 | import { CustomConfig } from '../../../../shared/models/server/custom-config.model' |
24 | import { User } from '../../../../shared/models/users' | 31 | import { User } from '../../../../shared/models/users' |
diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts index 9fefca7e3..0b4e66369 100644 --- a/server/tests/api/videos/video-privacy.ts +++ b/server/tests/api/videos/video-privacy.ts | |||
@@ -10,12 +10,12 @@ import { | |||
10 | ServerInfo, | 10 | ServerInfo, |
11 | setAccessTokensToServers, | 11 | setAccessTokensToServers, |
12 | uploadVideo | 12 | uploadVideo |
13 | } from '../../utils/index' | 13 | } from '../../../../shared/utils/index' |
14 | import { doubleFollow } from '../../utils/server/follows' | 14 | import { doubleFollow } from '../../../../shared/utils/server/follows' |
15 | import { userLogin } from '../../utils/users/login' | 15 | import { userLogin } from '../../../../shared/utils/users/login' |
16 | import { createUser } from '../../utils/users/users' | 16 | import { createUser } from '../../../../shared/utils/users/users' |
17 | import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../utils/videos/videos' | 17 | import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/utils/videos/videos' |
18 | import { waitJobs } from '../../utils/server/jobs' | 18 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
19 | 19 | ||
20 | const expect = chai.expect | 20 | const expect = chai.expect |
21 | 21 | ||
diff --git a/server/tests/api/videos/video-schedule-update.ts b/server/tests/api/videos/video-schedule-update.ts index a260fa4da..632c4244c 100644 --- a/server/tests/api/videos/video-schedule-update.ts +++ b/server/tests/api/videos/video-schedule-update.ts | |||
@@ -15,9 +15,8 @@ import { | |||
15 | updateVideo, | 15 | updateVideo, |
16 | uploadVideo, | 16 | uploadVideo, |
17 | wait | 17 | wait |
18 | } from '../../utils' | 18 | } from '../../../../shared/utils' |
19 | import { join } from 'path' | 19 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
20 | import { waitJobs } from '../../utils/server/jobs' | ||
21 | 20 | ||
22 | const expect = chai.expect | 21 | const expect = chai.expect |
23 | 22 | ||
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 0f83d4d57..eefd32ef8 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -3,13 +3,13 @@ | |||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
6 | import * as ffmpeg from 'fluent-ffmpeg' | 6 | import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' |
7 | import { VideoDetails, VideoState } from '../../../../shared/models/videos' | 7 | import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
8 | import { getVideoFileFPS, audio } from '../../../helpers/ffmpeg-utils' | ||
9 | import { | 8 | import { |
10 | buildAbsoluteFixturePath, | 9 | buildAbsoluteFixturePath, |
11 | doubleFollow, | 10 | doubleFollow, |
12 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
12 | generateHighBitrateVideo, | ||
13 | getMyVideos, | 13 | getMyVideos, |
14 | getVideo, | 14 | getVideo, |
15 | getVideosList, | 15 | getVideosList, |
@@ -19,9 +19,10 @@ import { | |||
19 | setAccessTokensToServers, | 19 | setAccessTokensToServers, |
20 | uploadVideo, | 20 | uploadVideo, |
21 | webtorrentAdd | 21 | webtorrentAdd |
22 | } from '../../utils' | 22 | } from '../../../../shared/utils' |
23 | import { join } from 'path' | 23 | import { extname, join } from 'path' |
24 | import { waitJobs } from '../../utils/server/jobs' | 24 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
25 | import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' | ||
25 | 26 | ||
26 | const expect = chai.expect | 27 | const expect = chai.expect |
27 | 28 | ||
@@ -121,7 +122,7 @@ describe('Test video transcoding', function () { | |||
121 | expect(videoDetails.files).to.have.lengthOf(4) | 122 | expect(videoDetails.files).to.have.lengthOf(4) |
122 | 123 | ||
123 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') | 124 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') |
124 | const probe = await audio.get(ffmpeg, path) | 125 | const probe = await audio.get(path) |
125 | 126 | ||
126 | if (probe.audioStream) { | 127 | if (probe.audioStream) { |
127 | expect(probe.audioStream[ 'codec_name' ]).to.be.equal('aac') | 128 | expect(probe.audioStream[ 'codec_name' ]).to.be.equal('aac') |
@@ -152,7 +153,7 @@ describe('Test video transcoding', function () { | |||
152 | 153 | ||
153 | expect(videoDetails.files).to.have.lengthOf(4) | 154 | expect(videoDetails.files).to.have.lengthOf(4) |
154 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') | 155 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') |
155 | const probe = await audio.get(ffmpeg, path) | 156 | const probe = await audio.get(path) |
156 | expect(probe).to.not.have.property('audioStream') | 157 | expect(probe).to.not.have.property('audioStream') |
157 | } | 158 | } |
158 | }) | 159 | }) |
@@ -177,9 +178,9 @@ describe('Test video transcoding', function () { | |||
177 | 178 | ||
178 | expect(videoDetails.files).to.have.lengthOf(4) | 179 | expect(videoDetails.files).to.have.lengthOf(4) |
179 | const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture) | 180 | const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture) |
180 | const fixtureVideoProbe = await audio.get(ffmpeg, fixturePath) | 181 | const fixtureVideoProbe = await audio.get(fixturePath) |
181 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') | 182 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') |
182 | const videoProbe = await audio.get(ffmpeg, path) | 183 | const videoProbe = await audio.get(path) |
183 | if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { | 184 | if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { |
184 | const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] | 185 | const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] |
185 | expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit)) | 186 | expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit)) |
@@ -228,7 +229,7 @@ describe('Test video transcoding', function () { | |||
228 | } | 229 | } |
229 | }) | 230 | }) |
230 | 231 | ||
231 | it('Should wait transcoding before publishing the video', async function () { | 232 | it('Should wait for transcoding before publishing the video', async function () { |
232 | this.timeout(80000) | 233 | this.timeout(80000) |
233 | 234 | ||
234 | { | 235 | { |
@@ -281,6 +282,73 @@ describe('Test video transcoding', function () { | |||
281 | } | 282 | } |
282 | }) | 283 | }) |
283 | 284 | ||
285 | it('Should respect maximum bitrate values', async function () { | ||
286 | this.timeout(160000) | ||
287 | |||
288 | let tempFixturePath: string | ||
289 | |||
290 | { | ||
291 | tempFixturePath = await generateHighBitrateVideo() | ||
292 | |||
293 | const bitrate = await getVideoFileBitrate(tempFixturePath) | ||
294 | expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS)) | ||
295 | } | ||
296 | |||
297 | const videoAttributes = { | ||
298 | name: 'high bitrate video', | ||
299 | description: 'high bitrate video', | ||
300 | fixture: tempFixturePath | ||
301 | } | ||
302 | |||
303 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) | ||
304 | |||
305 | await waitJobs(servers) | ||
306 | |||
307 | for (const server of servers) { | ||
308 | const res = await getVideosList(server.url) | ||
309 | |||
310 | const video = res.body.data.find(v => v.name === videoAttributes.name) | ||
311 | |||
312 | for (const resolution of ['240', '360', '480', '720', '1080']) { | ||
313 | const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4') | ||
314 | const bitrate = await getVideoFileBitrate(path) | ||
315 | const fps = await getVideoFileFPS(path) | ||
316 | const resolution2 = await getVideoFileResolution(path) | ||
317 | |||
318 | expect(resolution2.videoFileResolution.toString()).to.equal(resolution) | ||
319 | expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) | ||
320 | } | ||
321 | } | ||
322 | }) | ||
323 | |||
324 | it('Should accept and transcode additional extensions', async function () { | ||
325 | this.timeout(300000) | ||
326 | |||
327 | for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { | ||
328 | const videoAttributes = { | ||
329 | name: fixture, | ||
330 | fixture | ||
331 | } | ||
332 | |||
333 | await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes) | ||
334 | |||
335 | await waitJobs(servers) | ||
336 | |||
337 | for (const server of servers) { | ||
338 | const res = await getVideosList(server.url) | ||
339 | |||
340 | const video = res.body.data.find(v => v.name === videoAttributes.name) | ||
341 | const res2 = await getVideo(server.url, video.id) | ||
342 | const videoDetails = res2.body | ||
343 | |||
344 | expect(videoDetails.files).to.have.lengthOf(4) | ||
345 | |||
346 | const magnetUri = videoDetails.files[ 0 ].magnetUri | ||
347 | expect(magnetUri).to.contain('.mp4') | ||
348 | } | ||
349 | } | ||
350 | }) | ||
351 | |||
284 | after(async function () { | 352 | after(async function () { |
285 | killallServers(servers) | 353 | killallServers(servers) |
286 | }) | 354 | }) |
diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts new file mode 100644 index 000000000..59e37ad86 --- /dev/null +++ b/server/tests/api/videos/videos-filter.ts | |||
@@ -0,0 +1,130 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | createUser, | ||
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
9 | flushTests, | ||
10 | killallServers, | ||
11 | makeGetRequest, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | uploadVideo, | ||
15 | userLogin | ||
16 | } from '../../../../shared/utils' | ||
17 | import { Video, VideoPrivacy } from '../../../../shared/models/videos' | ||
18 | import { UserRole } from '../../../../shared/models/users' | ||
19 | |||
20 | const expect = chai.expect | ||
21 | |||
22 | async function getVideosNames (server: ServerInfo, token: string, filter: string, statusCodeExpected = 200) { | ||
23 | const paths = [ | ||
24 | '/api/v1/video-channels/root_channel/videos', | ||
25 | '/api/v1/accounts/root/videos', | ||
26 | '/api/v1/videos', | ||
27 | '/api/v1/search/videos' | ||
28 | ] | ||
29 | |||
30 | const videosResults: Video[][] = [] | ||
31 | |||
32 | for (const path of paths) { | ||
33 | const res = await makeGetRequest({ | ||
34 | url: server.url, | ||
35 | path, | ||
36 | token, | ||
37 | query: { | ||
38 | sort: 'createdAt', | ||
39 | filter | ||
40 | }, | ||
41 | statusCodeExpected | ||
42 | }) | ||
43 | |||
44 | videosResults.push(res.body.data.map(v => v.name)) | ||
45 | } | ||
46 | |||
47 | return videosResults | ||
48 | } | ||
49 | |||
50 | describe('Test videos filter validator', function () { | ||
51 | let servers: ServerInfo[] | ||
52 | |||
53 | // --------------------------------------------------------------- | ||
54 | |||
55 | before(async function () { | ||
56 | this.timeout(120000) | ||
57 | |||
58 | await flushTests() | ||
59 | |||
60 | servers = await flushAndRunMultipleServers(2) | ||
61 | |||
62 | await setAccessTokensToServers(servers) | ||
63 | |||
64 | for (const server of servers) { | ||
65 | const moderator = { username: 'moderator', password: 'my super password' } | ||
66 | await createUser( | ||
67 | server.url, | ||
68 | server.accessToken, | ||
69 | moderator.username, | ||
70 | moderator.password, | ||
71 | undefined, | ||
72 | undefined, | ||
73 | UserRole.MODERATOR | ||
74 | ) | ||
75 | server['moderatorAccessToken'] = await userLogin(server, moderator) | ||
76 | |||
77 | await uploadVideo(server.url, server.accessToken, { name: 'public ' + server.serverNumber }) | ||
78 | |||
79 | { | ||
80 | const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED } | ||
81 | await uploadVideo(server.url, server.accessToken, attributes) | ||
82 | } | ||
83 | |||
84 | { | ||
85 | const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE } | ||
86 | await uploadVideo(server.url, server.accessToken, attributes) | ||
87 | } | ||
88 | } | ||
89 | |||
90 | await doubleFollow(servers[0], servers[1]) | ||
91 | }) | ||
92 | |||
93 | describe('Check videos filter', function () { | ||
94 | |||
95 | it('Should display local videos', async function () { | ||
96 | for (const server of servers) { | ||
97 | const namesResults = await getVideosNames(server, server.accessToken, 'local') | ||
98 | for (const names of namesResults) { | ||
99 | expect(names).to.have.lengthOf(1) | ||
100 | expect(names[ 0 ]).to.equal('public ' + server.serverNumber) | ||
101 | } | ||
102 | } | ||
103 | }) | ||
104 | |||
105 | it('Should display all local videos by the admin or the moderator', async function () { | ||
106 | for (const server of servers) { | ||
107 | for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) { | ||
108 | |||
109 | const namesResults = await getVideosNames(server, token, 'all-local') | ||
110 | for (const names of namesResults) { | ||
111 | expect(names).to.have.lengthOf(3) | ||
112 | |||
113 | expect(names[ 0 ]).to.equal('public ' + server.serverNumber) | ||
114 | expect(names[ 1 ]).to.equal('unlisted ' + server.serverNumber) | ||
115 | expect(names[ 2 ]).to.equal('private ' + server.serverNumber) | ||
116 | } | ||
117 | } | ||
118 | } | ||
119 | }) | ||
120 | }) | ||
121 | |||
122 | after(async function () { | ||
123 | killallServers(servers) | ||
124 | |||
125 | // Keep the logs if the test failed | ||
126 | if (this['ok']) { | ||
127 | await flushTests() | ||
128 | } | ||
129 | }) | ||
130 | }) | ||
diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts index 6d289b288..f654a422b 100644 --- a/server/tests/api/videos/videos-history.ts +++ b/server/tests/api/videos/videos-history.ts | |||
@@ -3,17 +3,21 @@ | |||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { | 5 | import { |
6 | createUser, | ||
6 | flushTests, | 7 | flushTests, |
7 | getVideosListWithToken, | 8 | getVideosListWithToken, |
8 | getVideoWithToken, | 9 | getVideoWithToken, |
9 | killallServers, makePutBodyRequest, | 10 | killallServers, |
10 | runServer, searchVideoWithToken, | 11 | runServer, |
12 | searchVideoWithToken, | ||
11 | ServerInfo, | 13 | ServerInfo, |
12 | setAccessTokensToServers, | 14 | setAccessTokensToServers, |
13 | uploadVideo | 15 | updateMyUser, |
14 | } from '../../utils' | 16 | uploadVideo, |
17 | userLogin | ||
18 | } from '../../../../shared/utils' | ||
15 | import { Video, VideoDetails } from '../../../../shared/models/videos' | 19 | import { Video, VideoDetails } from '../../../../shared/models/videos' |
16 | import { userWatchVideo } from '../../utils/videos/video-history' | 20 | import { listMyVideosHistory, removeMyVideosHistory, userWatchVideo } from '../../../../shared/utils/videos/video-history' |
17 | 21 | ||
18 | const expect = chai.expect | 22 | const expect = chai.expect |
19 | 23 | ||
@@ -22,6 +26,8 @@ describe('Test videos history', function () { | |||
22 | let video1UUID: string | 26 | let video1UUID: string |
23 | let video2UUID: string | 27 | let video2UUID: string |
24 | let video3UUID: string | 28 | let video3UUID: string |
29 | let video3WatchedDate: Date | ||
30 | let userAccessToken: string | ||
25 | 31 | ||
26 | before(async function () { | 32 | before(async function () { |
27 | this.timeout(30000) | 33 | this.timeout(30000) |
@@ -46,6 +52,13 @@ describe('Test videos history', function () { | |||
46 | const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' }) | 52 | const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' }) |
47 | video3UUID = res.body.video.uuid | 53 | video3UUID = res.body.video.uuid |
48 | } | 54 | } |
55 | |||
56 | const user = { | ||
57 | username: 'user_1', | ||
58 | password: 'super password' | ||
59 | } | ||
60 | await createUser(server.url, server.accessToken, user.username, user.password) | ||
61 | userAccessToken = await userLogin(server, user) | ||
49 | }) | 62 | }) |
50 | 63 | ||
51 | it('Should get videos, without watching history', async function () { | 64 | it('Should get videos, without watching history', async function () { |
@@ -62,8 +75,8 @@ describe('Test videos history', function () { | |||
62 | }) | 75 | }) |
63 | 76 | ||
64 | it('Should watch the first and second video', async function () { | 77 | it('Should watch the first and second video', async function () { |
65 | await userWatchVideo(server.url, server.accessToken, video1UUID, 3) | ||
66 | await userWatchVideo(server.url, server.accessToken, video2UUID, 8) | 78 | await userWatchVideo(server.url, server.accessToken, video2UUID, 8) |
79 | await userWatchVideo(server.url, server.accessToken, video1UUID, 3) | ||
67 | }) | 80 | }) |
68 | 81 | ||
69 | it('Should return the correct history when listing, searching and getting videos', async function () { | 82 | it('Should return the correct history when listing, searching and getting videos', async function () { |
@@ -117,6 +130,68 @@ describe('Test videos history', function () { | |||
117 | } | 130 | } |
118 | }) | 131 | }) |
119 | 132 | ||
133 | it('Should have these videos when listing my history', async function () { | ||
134 | video3WatchedDate = new Date() | ||
135 | await userWatchVideo(server.url, server.accessToken, video3UUID, 2) | ||
136 | |||
137 | const res = await listMyVideosHistory(server.url, server.accessToken) | ||
138 | |||
139 | expect(res.body.total).to.equal(3) | ||
140 | |||
141 | const videos: Video[] = res.body.data | ||
142 | expect(videos[0].name).to.equal('video 3') | ||
143 | expect(videos[1].name).to.equal('video 1') | ||
144 | expect(videos[2].name).to.equal('video 2') | ||
145 | }) | ||
146 | |||
147 | it('Should not have videos history on another user', async function () { | ||
148 | const res = await listMyVideosHistory(server.url, userAccessToken) | ||
149 | |||
150 | expect(res.body.total).to.equal(0) | ||
151 | expect(res.body.data).to.have.lengthOf(0) | ||
152 | }) | ||
153 | |||
154 | it('Should clear my history', async function () { | ||
155 | await removeMyVideosHistory(server.url, server.accessToken, video3WatchedDate.toISOString()) | ||
156 | }) | ||
157 | |||
158 | it('Should have my history cleared', async function () { | ||
159 | const res = await listMyVideosHistory(server.url, server.accessToken) | ||
160 | |||
161 | expect(res.body.total).to.equal(1) | ||
162 | |||
163 | const videos: Video[] = res.body.data | ||
164 | expect(videos[0].name).to.equal('video 3') | ||
165 | }) | ||
166 | |||
167 | it('Should disable videos history', async function () { | ||
168 | await updateMyUser({ | ||
169 | url: server.url, | ||
170 | accessToken: server.accessToken, | ||
171 | videosHistoryEnabled: false | ||
172 | }) | ||
173 | |||
174 | await userWatchVideo(server.url, server.accessToken, video2UUID, 8, 409) | ||
175 | }) | ||
176 | |||
177 | it('Should re-enable videos history', async function () { | ||
178 | await updateMyUser({ | ||
179 | url: server.url, | ||
180 | accessToken: server.accessToken, | ||
181 | videosHistoryEnabled: true | ||
182 | }) | ||
183 | |||
184 | await userWatchVideo(server.url, server.accessToken, video1UUID, 8) | ||
185 | |||
186 | const res = await listMyVideosHistory(server.url, server.accessToken) | ||
187 | |||
188 | expect(res.body.total).to.equal(2) | ||
189 | |||
190 | const videos: Video[] = res.body.data | ||
191 | expect(videos[0].name).to.equal('video 1') | ||
192 | expect(videos[1].name).to.equal('video 3') | ||
193 | }) | ||
194 | |||
120 | after(async function () { | 195 | after(async function () { |
121 | killallServers([ server ]) | 196 | killallServers([ server ]) |
122 | 197 | ||
diff --git a/server/tests/api/videos/videos-overview.ts b/server/tests/api/videos/videos-overview.ts index 7d1f29c92..7221bcae6 100644 --- a/server/tests/api/videos/videos-overview.ts +++ b/server/tests/api/videos/videos-overview.ts | |||
@@ -2,8 +2,8 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils' | 5 | import { flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../../../shared/utils' |
6 | import { getVideosOverview } from '../../utils/overviews/overviews' | 6 | import { getVideosOverview } from '../../../../shared/utils/overviews/overviews' |
7 | import { VideosOverview } from '../../../../shared/models/overviews' | 7 | import { VideosOverview } from '../../../../shared/models/overviews' |
8 | 8 | ||
9 | const expect = chai.expect | 9 | const expect = chai.expect |
diff --git a/server/tests/cli/create-import-video-file-job.ts b/server/tests/cli/create-import-video-file-job.ts index 13bcfd209..4acda47b1 100644 --- a/server/tests/cli/create-import-video-file-job.ts +++ b/server/tests/cli/create-import-video-file-job.ts | |||
@@ -15,8 +15,8 @@ import { | |||
15 | ServerInfo, | 15 | ServerInfo, |
16 | setAccessTokensToServers, | 16 | setAccessTokensToServers, |
17 | uploadVideo | 17 | uploadVideo |
18 | } from '../utils' | 18 | } from '../../../shared/utils' |
19 | import { waitJobs } from '../utils/server/jobs' | 19 | import { waitJobs } from '../../../shared/utils/server/jobs' |
20 | 20 | ||
21 | const expect = chai.expect | 21 | const expect = chai.expect |
22 | 22 | ||
diff --git a/server/tests/cli/create-transcoding-job.ts b/server/tests/cli/create-transcoding-job.ts index c2e3840c5..50be5fa19 100644 --- a/server/tests/cli/create-transcoding-job.ts +++ b/server/tests/cli/create-transcoding-job.ts | |||
@@ -15,8 +15,8 @@ import { | |||
15 | ServerInfo, | 15 | ServerInfo, |
16 | setAccessTokensToServers, | 16 | setAccessTokensToServers, |
17 | uploadVideo, wait | 17 | uploadVideo, wait |
18 | } from '../utils' | 18 | } from '../../../shared/utils' |
19 | import { waitJobs } from '../utils/server/jobs' | 19 | import { waitJobs } from '../../../shared/utils/server/jobs' |
20 | 20 | ||
21 | const expect = chai.expect | 21 | const expect = chai.expect |
22 | 22 | ||
diff --git a/server/tests/cli/index.ts b/server/tests/cli/index.ts index 6201314ce..c6b7ec078 100644 --- a/server/tests/cli/index.ts +++ b/server/tests/cli/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | // Order of the tests we want to execute | 1 | // Order of the tests we want to execute |
2 | import './create-import-video-file-job' | 2 | import './create-import-video-file-job' |
3 | import './create-transcoding-job' | 3 | import './create-transcoding-job' |
4 | import './optimize-old-videos' | ||
4 | import './peertube' | 5 | import './peertube' |
5 | import './reset-password' | 6 | import './reset-password' |
6 | import './update-host' | 7 | import './update-host' |
diff --git a/server/tests/cli/optimize-old-videos.ts b/server/tests/cli/optimize-old-videos.ts new file mode 100644 index 000000000..6f6bc25a6 --- /dev/null +++ b/server/tests/cli/optimize-old-videos.ts | |||
@@ -0,0 +1,120 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { getMaxBitrate, Video, VideoDetails, VideoResolution } from '../../../shared/models/videos' | ||
6 | import { | ||
7 | doubleFollow, | ||
8 | execCLI, | ||
9 | flushAndRunMultipleServers, | ||
10 | flushTests, generateHighBitrateVideo, | ||
11 | getEnvCli, | ||
12 | getVideo, | ||
13 | getVideosList, | ||
14 | killallServers, root, | ||
15 | ServerInfo, | ||
16 | setAccessTokensToServers, | ||
17 | uploadVideo, viewVideo, wait | ||
18 | } from '../../../shared/utils' | ||
19 | import { waitJobs } from '../../../shared/utils/server/jobs' | ||
20 | import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils' | ||
21 | import { VIDEO_TRANSCODING_FPS } from '../../initializers' | ||
22 | import { join } from 'path' | ||
23 | |||
24 | const expect = chai.expect | ||
25 | |||
26 | describe('Test optimize old videos', function () { | ||
27 | let servers: ServerInfo[] = [] | ||
28 | let video1UUID: string | ||
29 | let video2UUID: string | ||
30 | |||
31 | before(async function () { | ||
32 | this.timeout(200000) | ||
33 | |||
34 | await flushTests() | ||
35 | |||
36 | // Run server 2 to have transcoding enabled | ||
37 | servers = await flushAndRunMultipleServers(2) | ||
38 | await setAccessTokensToServers(servers) | ||
39 | |||
40 | await doubleFollow(servers[0], servers[1]) | ||
41 | |||
42 | let tempFixturePath: string | ||
43 | |||
44 | { | ||
45 | tempFixturePath = await generateHighBitrateVideo() | ||
46 | |||
47 | const bitrate = await getVideoFileBitrate(tempFixturePath) | ||
48 | expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS)) | ||
49 | } | ||
50 | |||
51 | // Upload two videos for our needs | ||
52 | const res1 = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video1', fixture: tempFixturePath }) | ||
53 | video1UUID = res1.body.video.uuid | ||
54 | const res2 = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video2', fixture: tempFixturePath }) | ||
55 | video2UUID = res2.body.video.uuid | ||
56 | |||
57 | await waitJobs(servers) | ||
58 | }) | ||
59 | |||
60 | it('Should have two video files on each server', async function () { | ||
61 | this.timeout(30000) | ||
62 | |||
63 | for (const server of servers) { | ||
64 | const res = await getVideosList(server.url) | ||
65 | const videos = res.body.data | ||
66 | expect(videos).to.have.lengthOf(2) | ||
67 | |||
68 | for (const video of videos) { | ||
69 | const res2 = await getVideo(server.url, video.uuid) | ||
70 | const videoDetail: VideoDetails = res2.body | ||
71 | expect(videoDetail.files).to.have.lengthOf(1) | ||
72 | } | ||
73 | } | ||
74 | }) | ||
75 | |||
76 | it('Should run optimize script', async function () { | ||
77 | this.timeout(120000) | ||
78 | |||
79 | const env = getEnvCli(servers[0]) | ||
80 | await execCLI(`${env} npm run optimize-old-videos`) | ||
81 | |||
82 | await waitJobs(servers) | ||
83 | |||
84 | for (const server of servers) { | ||
85 | const res = await getVideosList(server.url) | ||
86 | const videos: Video[] = res.body.data | ||
87 | |||
88 | expect(videos).to.have.lengthOf(2) | ||
89 | |||
90 | for (const video of videos) { | ||
91 | await viewVideo(server.url, video.uuid) | ||
92 | |||
93 | // Refresh video | ||
94 | await waitJobs(servers) | ||
95 | await wait(5000) | ||
96 | await waitJobs(servers) | ||
97 | |||
98 | const res2 = await getVideo(server.url, video.uuid) | ||
99 | const videosDetails: VideoDetails = res2.body | ||
100 | |||
101 | expect(videosDetails.files).to.have.lengthOf(1) | ||
102 | const file = videosDetails.files[0] | ||
103 | |||
104 | expect(file.size).to.be.below(5000000) | ||
105 | |||
106 | const path = join(root(), 'test1', 'videos', video.uuid + '-' + file.resolution.id + '.mp4') | ||
107 | const bitrate = await getVideoFileBitrate(path) | ||
108 | const fps = await getVideoFileFPS(path) | ||
109 | const resolution = await getVideoFileResolution(path) | ||
110 | |||
111 | expect(resolution.videoFileResolution).to.equal(file.resolution.id) | ||
112 | expect(bitrate).to.be.below(getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) | ||
113 | } | ||
114 | } | ||
115 | }) | ||
116 | |||
117 | after(async function () { | ||
118 | killallServers(servers) | ||
119 | }) | ||
120 | }) | ||
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts index 65cb05a1a..e2836d0c3 100644 --- a/server/tests/cli/peertube.ts +++ b/server/tests/cli/peertube.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | runServer, | 11 | runServer, |
12 | ServerInfo, | 12 | ServerInfo, |
13 | setAccessTokensToServers | 13 | setAccessTokensToServers |
14 | } from '../utils' | 14 | } from '../../../shared/utils' |
15 | 15 | ||
16 | describe('Test CLI wrapper', function () { | 16 | describe('Test CLI wrapper', function () { |
17 | let server: ServerInfo | 17 | let server: ServerInfo |
@@ -44,6 +44,8 @@ describe('Test CLI wrapper', function () { | |||
44 | }) | 44 | }) |
45 | 45 | ||
46 | after(async function () { | 46 | after(async function () { |
47 | this.timeout(10000) | ||
48 | |||
47 | await execCLI(cmd + ` auth del ${server.url}`) | 49 | await execCLI(cmd + ` auth del ${server.url}`) |
48 | 50 | ||
49 | killallServers([ server ]) | 51 | killallServers([ server ]) |
diff --git a/server/tests/cli/reset-password.ts b/server/tests/cli/reset-password.ts index bf937d1c0..1b65f7e39 100644 --- a/server/tests/cli/reset-password.ts +++ b/server/tests/cli/reset-password.ts | |||
@@ -10,7 +10,7 @@ import { | |||
10 | runServer, | 10 | runServer, |
11 | ServerInfo, | 11 | ServerInfo, |
12 | setAccessTokensToServers | 12 | setAccessTokensToServers |
13 | } from '../utils' | 13 | } from '../../../shared/utils' |
14 | 14 | ||
15 | describe('Test reset password scripts', function () { | 15 | describe('Test reset password scripts', function () { |
16 | let server: ServerInfo | 16 | let server: ServerInfo |
diff --git a/server/tests/cli/update-host.ts b/server/tests/cli/update-host.ts index b89e72ab7..d38bb4331 100644 --- a/server/tests/cli/update-host.ts +++ b/server/tests/cli/update-host.ts | |||
@@ -3,8 +3,8 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { VideoDetails } from '../../../shared/models/videos' | 5 | import { VideoDetails } from '../../../shared/models/videos' |
6 | import { waitJobs } from '../utils/server/jobs' | 6 | import { waitJobs } from '../../../shared/utils/server/jobs' |
7 | import { addVideoCommentThread } from '../utils/videos/video-comments' | 7 | import { addVideoCommentThread } from '../../../shared/utils/videos/video-comments' |
8 | import { | 8 | import { |
9 | addVideoChannel, | 9 | addVideoChannel, |
10 | createUser, | 10 | createUser, |
@@ -21,8 +21,8 @@ import { | |||
21 | ServerInfo, | 21 | ServerInfo, |
22 | setAccessTokensToServers, | 22 | setAccessTokensToServers, |
23 | uploadVideo | 23 | uploadVideo |
24 | } from '../utils' | 24 | } from '../../../shared/utils' |
25 | import { getAccountsList } from '../utils/users/accounts' | 25 | import { getAccountsList } from '../../../shared/utils/users/accounts' |
26 | 26 | ||
27 | const expect = chai.expect | 27 | const expect = chai.expect |
28 | 28 | ||
@@ -86,6 +86,13 @@ describe('Test update host scripts', function () { | |||
86 | const { body } = await makeActivityPubGetRequest(server.url, '/videos/watch/' + video.uuid) | 86 | const { body } = await makeActivityPubGetRequest(server.url, '/videos/watch/' + video.uuid) |
87 | 87 | ||
88 | expect(body.id).to.equal('http://localhost:9002/videos/watch/' + video.uuid) | 88 | expect(body.id).to.equal('http://localhost:9002/videos/watch/' + video.uuid) |
89 | |||
90 | const res = await getVideo(server.url, video.uuid) | ||
91 | const videoDetails: VideoDetails = res.body | ||
92 | |||
93 | expect(videoDetails.trackerUrls[0]).to.include(server.host) | ||
94 | expect(videoDetails.streamingPlaylists[0].playlistUrl).to.include(server.host) | ||
95 | expect(videoDetails.streamingPlaylists[0].segmentsSha256Url).to.include(server.host) | ||
89 | } | 96 | } |
90 | }) | 97 | }) |
91 | 98 | ||
@@ -100,7 +107,7 @@ describe('Test update host scripts', function () { | |||
100 | } | 107 | } |
101 | }) | 108 | }) |
102 | 109 | ||
103 | it('Should have update accounts url', async function () { | 110 | it('Should have updated accounts url', async function () { |
104 | const res = await getAccountsList(server.url) | 111 | const res = await getAccountsList(server.url) |
105 | expect(res.body.total).to.equal(3) | 112 | expect(res.body.total).to.equal(3) |
106 | 113 | ||
@@ -112,7 +119,7 @@ describe('Test update host scripts', function () { | |||
112 | } | 119 | } |
113 | }) | 120 | }) |
114 | 121 | ||
115 | it('Should update torrent hosts', async function () { | 122 | it('Should have updated torrent hosts', async function () { |
116 | this.timeout(30000) | 123 | this.timeout(30000) |
117 | 124 | ||
118 | const res = await getVideosList(server.url) | 125 | const res = await getVideosList(server.url) |
diff --git a/server/tests/client.ts b/server/tests/client.ts index b33a653b1..06b4a9c5a 100644 --- a/server/tests/client.ts +++ b/server/tests/client.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | updateCustomConfig, | 15 | updateCustomConfig, |
16 | updateCustomSubConfig, | 16 | updateCustomSubConfig, |
17 | uploadVideo | 17 | uploadVideo |
18 | } from './utils' | 18 | } from '../../shared/utils' |
19 | 19 | ||
20 | const expect = chai.expect | 20 | const expect = chai.expect |
21 | 21 | ||
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index 28fe3493b..a771474bc 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -13,10 +13,10 @@ import { | |||
13 | ServerInfo, | 13 | ServerInfo, |
14 | setAccessTokensToServers, | 14 | setAccessTokensToServers, |
15 | uploadVideo, userLogin | 15 | uploadVideo, userLogin |
16 | } from '../utils' | 16 | } from '../../../shared/utils' |
17 | import * as libxmljs from 'libxmljs' | 17 | import * as libxmljs from 'libxmljs' |
18 | import { addVideoCommentThread } from '../utils/videos/video-comments' | 18 | import { addVideoCommentThread } from '../../../shared/utils/videos/video-comments' |
19 | import { waitJobs } from '../utils/server/jobs' | 19 | import { waitJobs } from '../../../shared/utils/server/jobs' |
20 | import { User } from '../../../shared/models/users' | 20 | import { User } from '../../../shared/models/users' |
21 | 21 | ||
22 | chai.use(require('chai-xml')) | 22 | chai.use(require('chai-xml')) |
diff --git a/server/tests/fixtures/video_short.avi b/server/tests/fixtures/video_short.avi new file mode 100644 index 000000000..88979cab2 --- /dev/null +++ b/server/tests/fixtures/video_short.avi | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short.mkv b/server/tests/fixtures/video_short.mkv new file mode 100644 index 000000000..a67f4f806 --- /dev/null +++ b/server/tests/fixtures/video_short.mkv | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short_240p.mp4 b/server/tests/fixtures/video_short_240p.mp4 new file mode 100644 index 000000000..db074940b --- /dev/null +++ b/server/tests/fixtures/video_short_240p.mp4 | |||
Binary files differ | |||
diff --git a/server/tests/helpers/comment-model.ts b/server/tests/helpers/comment-model.ts new file mode 100644 index 000000000..76bb0f212 --- /dev/null +++ b/server/tests/helpers/comment-model.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { VideoCommentModel } from '../../models/video/video-comment' | ||
6 | |||
7 | const expect = chai.expect | ||
8 | |||
9 | class CommentMock { | ||
10 | text: string | ||
11 | |||
12 | extractMentions = VideoCommentModel.prototype.extractMentions | ||
13 | } | ||
14 | |||
15 | describe('Comment model', function () { | ||
16 | it('Should correctly extract mentions', async function () { | ||
17 | const comment = new CommentMock() | ||
18 | |||
19 | comment.text = '@florian @jean@localhost:9000 @flo @another@localhost:9000 @flo2@jean.com hello ' + | ||
20 | 'email@localhost:9000 coucou.com no? @chocobozzz @chocobozzz @end' | ||
21 | const result = comment.extractMentions().sort() | ||
22 | |||
23 | expect(result).to.deep.equal([ 'another', 'chocobozzz', 'end', 'flo', 'florian', 'jean' ]) | ||
24 | }) | ||
25 | }) | ||
diff --git a/server/tests/helpers/core-utils.ts b/server/tests/helpers/core-utils.ts new file mode 100644 index 000000000..e604cf7e3 --- /dev/null +++ b/server/tests/helpers/core-utils.ts | |||
@@ -0,0 +1,98 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { snakeCase, isNumber } from 'lodash' | ||
6 | import { | ||
7 | parseBytes, objectConverter | ||
8 | } from '../../helpers/core-utils' | ||
9 | import { isNumeric } from 'validator' | ||
10 | |||
11 | const expect = chai.expect | ||
12 | |||
13 | describe('Parse Bytes', function () { | ||
14 | |||
15 | it('Should pass when given valid value', async function () { | ||
16 | // just return it | ||
17 | expect(parseBytes(1024)).to.be.eq(1024) | ||
18 | expect(parseBytes(1048576)).to.be.eq(1048576) | ||
19 | expect(parseBytes('1024')).to.be.eq(1024) | ||
20 | expect(parseBytes('1048576')).to.be.eq(1048576) | ||
21 | |||
22 | // sizes | ||
23 | expect(parseBytes('1B')).to.be.eq(1024) | ||
24 | expect(parseBytes('1MB')).to.be.eq(1048576) | ||
25 | expect(parseBytes('1GB')).to.be.eq(1073741824) | ||
26 | expect(parseBytes('1TB')).to.be.eq(1099511627776) | ||
27 | |||
28 | expect(parseBytes('5GB')).to.be.eq(5368709120) | ||
29 | expect(parseBytes('5TB')).to.be.eq(5497558138880) | ||
30 | |||
31 | expect(parseBytes('1024B')).to.be.eq(1048576) | ||
32 | expect(parseBytes('1024MB')).to.be.eq(1073741824) | ||
33 | expect(parseBytes('1024GB')).to.be.eq(1099511627776) | ||
34 | expect(parseBytes('1024TB')).to.be.eq(1125899906842624) | ||
35 | |||
36 | // with whitespace | ||
37 | expect(parseBytes('1 GB')).to.be.eq(1073741824) | ||
38 | expect(parseBytes('1\tGB')).to.be.eq(1073741824) | ||
39 | |||
40 | // sum value | ||
41 | expect(parseBytes('1TB 1024MB')).to.be.eq(1100585369600) | ||
42 | expect(parseBytes('4GB 1024MB')).to.be.eq(5368709120) | ||
43 | expect(parseBytes('4TB 1024GB')).to.be.eq(5497558138880) | ||
44 | expect(parseBytes('4TB 1024GB 0MB')).to.be.eq(5497558138880) | ||
45 | expect(parseBytes('1024TB 1024GB 1024MB')).to.be.eq(1127000492212224) | ||
46 | }) | ||
47 | |||
48 | it('Should be invalid when given invalid value', async function () { | ||
49 | expect(parseBytes('6GB 1GB')).to.be.eq(6) | ||
50 | }) | ||
51 | |||
52 | it('Should convert an object', async function () { | ||
53 | function keyConverter (k: string) { | ||
54 | return snakeCase(k) | ||
55 | } | ||
56 | |||
57 | function valueConverter (v: any) { | ||
58 | if (isNumeric(v + '')) return parseInt('' + v, 10) | ||
59 | |||
60 | return v | ||
61 | } | ||
62 | |||
63 | const obj = { | ||
64 | mySuperKey: 'hello', | ||
65 | mySuper2Key: '45', | ||
66 | mySuper3Key: { | ||
67 | mySuperSubKey: '15', | ||
68 | mySuperSub2Key: 'hello', | ||
69 | mySuperSub3Key: [ '1', 'hello', 2 ], | ||
70 | mySuperSub4Key: 4 | ||
71 | }, | ||
72 | mySuper4Key: 45, | ||
73 | toto: { | ||
74 | super_key: '15', | ||
75 | superKey2: 'hello' | ||
76 | }, | ||
77 | super_key: { | ||
78 | superKey4: 15 | ||
79 | } | ||
80 | } | ||
81 | |||
82 | const res = objectConverter(obj, keyConverter, valueConverter) | ||
83 | |||
84 | expect(res.my_super_key).to.equal('hello') | ||
85 | expect(res.my_super_2_key).to.equal(45) | ||
86 | expect(res.my_super_3_key.my_super_sub_key).to.equal(15) | ||
87 | expect(res.my_super_3_key.my_super_sub_2_key).to.equal('hello') | ||
88 | expect(res.my_super_3_key.my_super_sub_3_key).to.deep.equal([ 1, 'hello', 2 ]) | ||
89 | expect(res.my_super_3_key.my_super_sub_4_key).to.equal(4) | ||
90 | expect(res.toto.super_key).to.equal(15) | ||
91 | expect(res.toto.super_key_2).to.equal('hello') | ||
92 | expect(res.super_key.super_key_4).to.equal(15) | ||
93 | |||
94 | // Immutable | ||
95 | expect(res.mySuperKey).to.be.undefined | ||
96 | expect(obj['my_super_key']).to.be.undefined | ||
97 | }) | ||
98 | }) | ||
diff --git a/server/tests/helpers/index.ts b/server/tests/helpers/index.ts new file mode 100644 index 000000000..551208245 --- /dev/null +++ b/server/tests/helpers/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | import './core-utils' | ||
2 | import './comment-model' | ||
diff --git a/server/tests/index.ts b/server/tests/index.ts index e659fd3df..ed16d65dd 100644 --- a/server/tests/index.ts +++ b/server/tests/index.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | // Order of the tests we want to execute | 1 | // Order of the tests we want to execute |
2 | import './client' | 2 | import './client' |
3 | import './activitypub' | ||
4 | import './feeds/' | 3 | import './feeds/' |
5 | import './cli/' | 4 | import './cli/' |
6 | import './api/' | 5 | import './api/' |
diff --git a/server/tests/misc-endpoints.ts b/server/tests/misc-endpoints.ts index 8fab20971..5f82719da 100644 --- a/server/tests/misc-endpoints.ts +++ b/server/tests/misc-endpoints.ts | |||
@@ -2,7 +2,18 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { flushTests, killallServers, makeGetRequest, runServer, ServerInfo } from './utils' | 5 | import { |
6 | addVideoChannel, | ||
7 | createUser, | ||
8 | flushTests, | ||
9 | killallServers, | ||
10 | makeGetRequest, | ||
11 | runServer, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | uploadVideo | ||
15 | } from '../../shared/utils' | ||
16 | import { VideoPrivacy } from '../../shared/models/videos' | ||
6 | 17 | ||
7 | const expect = chai.expect | 18 | const expect = chai.expect |
8 | 19 | ||
@@ -15,6 +26,7 @@ describe('Test misc endpoints', function () { | |||
15 | await flushTests() | 26 | await flushTests() |
16 | 27 | ||
17 | server = await runServer(1) | 28 | server = await runServer(1) |
29 | await setAccessTokensToServers([ server ]) | ||
18 | }) | 30 | }) |
19 | 31 | ||
20 | describe('Test a well known endpoints', function () { | 32 | describe('Test a well known endpoints', function () { |
@@ -60,6 +72,16 @@ describe('Test misc endpoints', function () { | |||
60 | 72 | ||
61 | expect(res.body.tracking).to.equal('N') | 73 | expect(res.body.tracking).to.equal('N') |
62 | }) | 74 | }) |
75 | |||
76 | it('Should get change-password location', async function () { | ||
77 | const res = await makeGetRequest({ | ||
78 | url: server.url, | ||
79 | path: '/.well-known/change-password', | ||
80 | statusCodeExpected: 302 | ||
81 | }) | ||
82 | |||
83 | expect(res.header.location).to.equal('/my-account/settings') | ||
84 | }) | ||
63 | }) | 85 | }) |
64 | 86 | ||
65 | describe('Test classic static endpoints', function () { | 87 | describe('Test classic static endpoints', function () { |
@@ -93,6 +115,64 @@ describe('Test misc endpoints', function () { | |||
93 | }) | 115 | }) |
94 | }) | 116 | }) |
95 | 117 | ||
118 | describe('Test bots endpoints', function () { | ||
119 | |||
120 | it('Should get the empty sitemap', async function () { | ||
121 | const res = await makeGetRequest({ | ||
122 | url: server.url, | ||
123 | path: '/sitemap.xml', | ||
124 | statusCodeExpected: 200 | ||
125 | }) | ||
126 | |||
127 | expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"') | ||
128 | expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>') | ||
129 | }) | ||
130 | |||
131 | it('Should get the empty cached sitemap', async function () { | ||
132 | const res = await makeGetRequest({ | ||
133 | url: server.url, | ||
134 | path: '/sitemap.xml', | ||
135 | statusCodeExpected: 200 | ||
136 | }) | ||
137 | |||
138 | expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"') | ||
139 | expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>') | ||
140 | }) | ||
141 | |||
142 | it('Should add videos, channel and accounts and get sitemap', async function () { | ||
143 | this.timeout(35000) | ||
144 | |||
145 | await uploadVideo(server.url, server.accessToken, { name: 'video 1', nsfw: false }) | ||
146 | await uploadVideo(server.url, server.accessToken, { name: 'video 2', nsfw: false }) | ||
147 | await uploadVideo(server.url, server.accessToken, { name: 'video 3', privacy: VideoPrivacy.PRIVATE }) | ||
148 | |||
149 | await addVideoChannel(server.url, server.accessToken, { name: 'channel1', displayName: 'channel 1' }) | ||
150 | await addVideoChannel(server.url, server.accessToken, { name: 'channel2', displayName: 'channel 2' }) | ||
151 | |||
152 | await createUser(server.url, server.accessToken, 'user1', 'password') | ||
153 | await createUser(server.url, server.accessToken, 'user2', 'password') | ||
154 | |||
155 | const res = await makeGetRequest({ | ||
156 | url: server.url, | ||
157 | path: '/sitemap.xml?t=1', // avoid using cache | ||
158 | statusCodeExpected: 200 | ||
159 | }) | ||
160 | |||
161 | expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"') | ||
162 | expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>') | ||
163 | |||
164 | expect(res.text).to.contain('<video:title><![CDATA[video 1]]></video:title>') | ||
165 | expect(res.text).to.contain('<video:title><![CDATA[video 2]]></video:title>') | ||
166 | expect(res.text).to.not.contain('<video:title><![CDATA[video 3]]></video:title>') | ||
167 | |||
168 | expect(res.text).to.contain('<url><loc>http://localhost:9001/video-channels/channel1</loc></url>') | ||
169 | expect(res.text).to.contain('<url><loc>http://localhost:9001/video-channels/channel2</loc></url>') | ||
170 | |||
171 | expect(res.text).to.contain('<url><loc>http://localhost:9001/accounts/user1</loc></url>') | ||
172 | expect(res.text).to.contain('<url><loc>http://localhost:9001/accounts/user2</loc></url>') | ||
173 | }) | ||
174 | }) | ||
175 | |||
96 | after(async function () { | 176 | after(async function () { |
97 | killallServers([ server ]) | 177 | killallServers([ server ]) |
98 | }) | 178 | }) |
diff --git a/server/tests/real-world/populate-database.ts b/server/tests/real-world/populate-database.ts index a7fdbd1dc..016503498 100644 --- a/server/tests/real-world/populate-database.ts +++ b/server/tests/real-world/populate-database.ts | |||
@@ -10,7 +10,7 @@ import { | |||
10 | ServerInfo, | 10 | ServerInfo, |
11 | setAccessTokensToServers, | 11 | setAccessTokensToServers, |
12 | uploadVideo | 12 | uploadVideo |
13 | } from '../utils' | 13 | } from '../../../shared/utils' |
14 | import * as Bluebird from 'bluebird' | 14 | import * as Bluebird from 'bluebird' |
15 | 15 | ||
16 | start() | 16 | start() |
diff --git a/server/tests/real-world/real-world.ts b/server/tests/real-world/real-world.ts index a96469b11..ac3baaf9a 100644 --- a/server/tests/real-world/real-world.ts +++ b/server/tests/real-world/real-world.ts | |||
@@ -16,8 +16,8 @@ import { | |||
16 | updateVideo, | 16 | updateVideo, |
17 | uploadVideo, viewVideo, | 17 | uploadVideo, viewVideo, |
18 | wait | 18 | wait |
19 | } from '../utils' | 19 | } from '../../../shared/utils' |
20 | import { getJobsListPaginationAndSort } from '../utils/server/jobs' | 20 | import { getJobsListPaginationAndSort } from '../../../shared/utils/server/jobs' |
21 | 21 | ||
22 | interface ServerInfo extends DefaultServerInfo { | 22 | interface ServerInfo extends DefaultServerInfo { |
23 | requestsNumber: number | 23 | requestsNumber: number |
diff --git a/server/tests/utils/cli/cli.ts b/server/tests/utils/cli/cli.ts deleted file mode 100644 index 54d05e9c6..000000000 --- a/server/tests/utils/cli/cli.ts +++ /dev/null | |||
@@ -1,24 +0,0 @@ | |||
1 | import { exec } from 'child_process' | ||
2 | |||
3 | import { ServerInfo } from '../server/servers' | ||
4 | |||
5 | function getEnvCli (server?: ServerInfo) { | ||
6 | return `NODE_ENV=test NODE_APP_INSTANCE=${server.serverNumber}` | ||
7 | } | ||
8 | |||
9 | async function execCLI (command: string) { | ||
10 | return new Promise<string>((res, rej) => { | ||
11 | exec(command, (err, stdout, stderr) => { | ||
12 | if (err) return rej(err) | ||
13 | |||
14 | return res(stdout) | ||
15 | }) | ||
16 | }) | ||
17 | } | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | export { | ||
22 | execCLI, | ||
23 | getEnvCli | ||
24 | } | ||
diff --git a/server/tests/utils/feeds/feeds.ts b/server/tests/utils/feeds/feeds.ts deleted file mode 100644 index af6df2b20..000000000 --- a/server/tests/utils/feeds/feeds.ts +++ /dev/null | |||
@@ -1,32 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | |||
3 | type FeedType = 'videos' | 'video-comments' | ||
4 | |||
5 | function getXMLfeed (url: string, feed: FeedType, format?: string) { | ||
6 | const path = '/feeds/' + feed + '.xml' | ||
7 | |||
8 | return request(url) | ||
9 | .get(path) | ||
10 | .query((format) ? { format: format } : {}) | ||
11 | .set('Accept', 'application/xml') | ||
12 | .expect(200) | ||
13 | .expect('Content-Type', /xml/) | ||
14 | } | ||
15 | |||
16 | function getJSONfeed (url: string, feed: FeedType, query: any = {}) { | ||
17 | const path = '/feeds/' + feed + '.json' | ||
18 | |||
19 | return request(url) | ||
20 | .get(path) | ||
21 | .query(query) | ||
22 | .set('Accept', 'application/json') | ||
23 | .expect(200) | ||
24 | .expect('Content-Type', /json/) | ||
25 | } | ||
26 | |||
27 | // --------------------------------------------------------------------------- | ||
28 | |||
29 | export { | ||
30 | getXMLfeed, | ||
31 | getJSONfeed | ||
32 | } | ||
diff --git a/server/tests/utils/index.ts b/server/tests/utils/index.ts deleted file mode 100644 index 897389824..000000000 --- a/server/tests/utils/index.ts +++ /dev/null | |||
@@ -1,18 +0,0 @@ | |||
1 | export * from './server/activitypub' | ||
2 | export * from './cli/cli' | ||
3 | export * from './server/clients' | ||
4 | export * from './server/config' | ||
5 | export * from './users/login' | ||
6 | export * from './miscs/miscs' | ||
7 | export * from './server/follows' | ||
8 | export * from './requests/requests' | ||
9 | export * from './server/servers' | ||
10 | export * from './videos/services' | ||
11 | export * from './users/users' | ||
12 | export * from './videos/video-abuses' | ||
13 | export * from './videos/video-blacklist' | ||
14 | export * from './videos/video-channels' | ||
15 | export * from './videos/videos' | ||
16 | export * from './videos/video-change-ownership' | ||
17 | export * from './feeds/feeds' | ||
18 | export * from './search/videos' | ||
diff --git a/server/tests/utils/miscs/email.ts b/server/tests/utils/miscs/email.ts deleted file mode 100644 index 21accd09d..000000000 --- a/server/tests/utils/miscs/email.ts +++ /dev/null | |||
@@ -1,25 +0,0 @@ | |||
1 | import * as MailDev from 'maildev' | ||
2 | |||
3 | function mockSmtpServer (emailsCollection: object[]) { | ||
4 | const maildev = new MailDev({ | ||
5 | ip: '127.0.0.1', | ||
6 | smtp: 1025, | ||
7 | disableWeb: true, | ||
8 | silent: true | ||
9 | }) | ||
10 | maildev.on('new', email => emailsCollection.push(email)) | ||
11 | |||
12 | return new Promise((res, rej) => { | ||
13 | maildev.listen(err => { | ||
14 | if (err) return rej(err) | ||
15 | |||
16 | return res() | ||
17 | }) | ||
18 | }) | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | export { | ||
24 | mockSmtpServer | ||
25 | } | ||
diff --git a/server/tests/utils/miscs/miscs.ts b/server/tests/utils/miscs/miscs.ts deleted file mode 100644 index b2f80e9b1..000000000 --- a/server/tests/utils/miscs/miscs.ts +++ /dev/null | |||
@@ -1,72 +0,0 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import { isAbsolute, join } from 'path' | ||
5 | import * as request from 'supertest' | ||
6 | import * as WebTorrent from 'webtorrent' | ||
7 | import { readFile } from 'fs-extra' | ||
8 | |||
9 | const expect = chai.expect | ||
10 | let webtorrent = new WebTorrent() | ||
11 | |||
12 | function immutableAssign <T, U> (target: T, source: U) { | ||
13 | return Object.assign<{}, T, U>({}, target, source) | ||
14 | } | ||
15 | |||
16 | // Default interval -> 5 minutes | ||
17 | function dateIsValid (dateString: string, interval = 300000) { | ||
18 | const dateToCheck = new Date(dateString) | ||
19 | const now = new Date() | ||
20 | |||
21 | return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval | ||
22 | } | ||
23 | |||
24 | function wait (milliseconds: number) { | ||
25 | return new Promise(resolve => setTimeout(resolve, milliseconds)) | ||
26 | } | ||
27 | |||
28 | function webtorrentAdd (torrent: string, refreshWebTorrent = false) { | ||
29 | if (refreshWebTorrent === true) webtorrent = new WebTorrent() | ||
30 | |||
31 | return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res)) | ||
32 | } | ||
33 | |||
34 | function root () { | ||
35 | // We are in server/tests/utils/miscs | ||
36 | return join(__dirname, '..', '..', '..', '..') | ||
37 | } | ||
38 | |||
39 | async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') { | ||
40 | const res = await request(url) | ||
41 | .get(imagePath) | ||
42 | .expect(200) | ||
43 | |||
44 | const body = res.body | ||
45 | |||
46 | const data = await readFile(join(__dirname, '..', '..', 'fixtures', imageName + extension)) | ||
47 | const minLength = body.length - ((20 * body.length) / 100) | ||
48 | const maxLength = body.length + ((20 * body.length) / 100) | ||
49 | |||
50 | expect(data.length).to.be.above(minLength) | ||
51 | expect(data.length).to.be.below(maxLength) | ||
52 | } | ||
53 | |||
54 | function buildAbsoluteFixturePath (path: string) { | ||
55 | if (isAbsolute(path)) { | ||
56 | return path | ||
57 | } | ||
58 | |||
59 | return join(__dirname, '..', '..', 'fixtures', path) | ||
60 | } | ||
61 | |||
62 | // --------------------------------------------------------------------------- | ||
63 | |||
64 | export { | ||
65 | dateIsValid, | ||
66 | wait, | ||
67 | webtorrentAdd, | ||
68 | immutableAssign, | ||
69 | testImage, | ||
70 | buildAbsoluteFixturePath, | ||
71 | root | ||
72 | } | ||
diff --git a/server/tests/utils/overviews/overviews.ts b/server/tests/utils/overviews/overviews.ts deleted file mode 100644 index 23e3ceb1e..000000000 --- a/server/tests/utils/overviews/overviews.ts +++ /dev/null | |||
@@ -1,18 +0,0 @@ | |||
1 | import { makeGetRequest } from '../requests/requests' | ||
2 | |||
3 | function getVideosOverview (url: string, useCache = false) { | ||
4 | const path = '/api/v1/overviews/videos' | ||
5 | |||
6 | const query = { | ||
7 | t: useCache ? undefined : new Date().getTime() | ||
8 | } | ||
9 | |||
10 | return makeGetRequest({ | ||
11 | url, | ||
12 | path, | ||
13 | query, | ||
14 | statusCodeExpected: 200 | ||
15 | }) | ||
16 | } | ||
17 | |||
18 | export { getVideosOverview } | ||
diff --git a/server/tests/utils/requests/check-api-params.ts b/server/tests/utils/requests/check-api-params.ts deleted file mode 100644 index edb47e0e9..000000000 --- a/server/tests/utils/requests/check-api-params.ts +++ /dev/null | |||
@@ -1,40 +0,0 @@ | |||
1 | import { makeGetRequest } from './requests' | ||
2 | import { immutableAssign } from '..' | ||
3 | |||
4 | function checkBadStartPagination (url: string, path: string, token?: string, query = {}) { | ||
5 | return makeGetRequest({ | ||
6 | url, | ||
7 | path, | ||
8 | token, | ||
9 | query: immutableAssign(query, { start: 'hello' }), | ||
10 | statusCodeExpected: 400 | ||
11 | }) | ||
12 | } | ||
13 | |||
14 | function checkBadCountPagination (url: string, path: string, token?: string, query = {}) { | ||
15 | return makeGetRequest({ | ||
16 | url, | ||
17 | path, | ||
18 | token, | ||
19 | query: immutableAssign(query, { count: 'hello' }), | ||
20 | statusCodeExpected: 400 | ||
21 | }) | ||
22 | } | ||
23 | |||
24 | function checkBadSortPagination (url: string, path: string, token?: string, query = {}) { | ||
25 | return makeGetRequest({ | ||
26 | url, | ||
27 | path, | ||
28 | token, | ||
29 | query: immutableAssign(query, { sort: 'hello' }), | ||
30 | statusCodeExpected: 400 | ||
31 | }) | ||
32 | } | ||
33 | |||
34 | // --------------------------------------------------------------------------- | ||
35 | |||
36 | export { | ||
37 | checkBadStartPagination, | ||
38 | checkBadCountPagination, | ||
39 | checkBadSortPagination | ||
40 | } | ||
diff --git a/server/tests/utils/requests/requests.ts b/server/tests/utils/requests/requests.ts deleted file mode 100644 index 27a529eda..000000000 --- a/server/tests/utils/requests/requests.ts +++ /dev/null | |||
@@ -1,170 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import { buildAbsoluteFixturePath } from '../miscs/miscs' | ||
3 | import { isAbsolute, join } from 'path' | ||
4 | |||
5 | function makeGetRequest (options: { | ||
6 | url: string, | ||
7 | path: string, | ||
8 | query?: any, | ||
9 | token?: string, | ||
10 | statusCodeExpected?: number, | ||
11 | contentType?: string | ||
12 | }) { | ||
13 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 | ||
14 | if (options.contentType === undefined) options.contentType = 'application/json' | ||
15 | |||
16 | const req = request(options.url) | ||
17 | .get(options.path) | ||
18 | |||
19 | if (options.contentType) req.set('Accept', options.contentType) | ||
20 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | ||
21 | if (options.query) req.query(options.query) | ||
22 | |||
23 | return req.expect(options.statusCodeExpected) | ||
24 | } | ||
25 | |||
26 | function makeDeleteRequest (options: { | ||
27 | url: string, | ||
28 | path: string, | ||
29 | token?: string, | ||
30 | statusCodeExpected?: number | ||
31 | }) { | ||
32 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 | ||
33 | |||
34 | const req = request(options.url) | ||
35 | .delete(options.path) | ||
36 | .set('Accept', 'application/json') | ||
37 | |||
38 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | ||
39 | |||
40 | return req | ||
41 | .expect('Content-Type', /json/) | ||
42 | .expect(options.statusCodeExpected) | ||
43 | } | ||
44 | |||
45 | function makeUploadRequest (options: { | ||
46 | url: string, | ||
47 | method?: 'POST' | 'PUT', | ||
48 | path: string, | ||
49 | token?: string, | ||
50 | fields: { [ fieldName: string ]: any }, | ||
51 | attaches: { [ attachName: string ]: any | any[] }, | ||
52 | statusCodeExpected?: number | ||
53 | }) { | ||
54 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 | ||
55 | |||
56 | let req: request.Test | ||
57 | if (options.method === 'PUT') { | ||
58 | req = request(options.url).put(options.path) | ||
59 | } else { | ||
60 | req = request(options.url).post(options.path) | ||
61 | } | ||
62 | |||
63 | req.set('Accept', 'application/json') | ||
64 | |||
65 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | ||
66 | |||
67 | Object.keys(options.fields).forEach(field => { | ||
68 | const value = options.fields[field] | ||
69 | |||
70 | if (Array.isArray(value)) { | ||
71 | for (let i = 0; i < value.length; i++) { | ||
72 | req.field(field + '[' + i + ']', value[i]) | ||
73 | } | ||
74 | } else { | ||
75 | req.field(field, value) | ||
76 | } | ||
77 | }) | ||
78 | |||
79 | Object.keys(options.attaches).forEach(attach => { | ||
80 | const value = options.attaches[attach] | ||
81 | if (Array.isArray(value)) { | ||
82 | req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1]) | ||
83 | } else { | ||
84 | req.attach(attach, buildAbsoluteFixturePath(value)) | ||
85 | } | ||
86 | }) | ||
87 | |||
88 | return req.expect(options.statusCodeExpected) | ||
89 | } | ||
90 | |||
91 | function makePostBodyRequest (options: { | ||
92 | url: string, | ||
93 | path: string, | ||
94 | token?: string, | ||
95 | fields?: { [ fieldName: string ]: any }, | ||
96 | statusCodeExpected?: number | ||
97 | }) { | ||
98 | if (!options.fields) options.fields = {} | ||
99 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 | ||
100 | |||
101 | const req = request(options.url) | ||
102 | .post(options.path) | ||
103 | .set('Accept', 'application/json') | ||
104 | |||
105 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | ||
106 | |||
107 | return req.send(options.fields) | ||
108 | .expect(options.statusCodeExpected) | ||
109 | } | ||
110 | |||
111 | function makePutBodyRequest (options: { | ||
112 | url: string, | ||
113 | path: string, | ||
114 | token?: string, | ||
115 | fields: { [ fieldName: string ]: any }, | ||
116 | statusCodeExpected?: number | ||
117 | }) { | ||
118 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 | ||
119 | |||
120 | const req = request(options.url) | ||
121 | .put(options.path) | ||
122 | .set('Accept', 'application/json') | ||
123 | |||
124 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | ||
125 | |||
126 | return req.send(options.fields) | ||
127 | .expect(options.statusCodeExpected) | ||
128 | } | ||
129 | |||
130 | function makeHTMLRequest (url: string, path: string) { | ||
131 | return request(url) | ||
132 | .get(path) | ||
133 | .set('Accept', 'text/html') | ||
134 | .expect(200) | ||
135 | } | ||
136 | |||
137 | function updateAvatarRequest (options: { | ||
138 | url: string, | ||
139 | path: string, | ||
140 | accessToken: string, | ||
141 | fixture: string | ||
142 | }) { | ||
143 | let filePath = '' | ||
144 | if (isAbsolute(options.fixture)) { | ||
145 | filePath = options.fixture | ||
146 | } else { | ||
147 | filePath = join(__dirname, '..', '..', 'fixtures', options.fixture) | ||
148 | } | ||
149 | |||
150 | return makeUploadRequest({ | ||
151 | url: options.url, | ||
152 | path: options.path, | ||
153 | token: options.accessToken, | ||
154 | fields: {}, | ||
155 | attaches: { avatarfile: filePath }, | ||
156 | statusCodeExpected: 200 | ||
157 | }) | ||
158 | } | ||
159 | |||
160 | // --------------------------------------------------------------------------- | ||
161 | |||
162 | export { | ||
163 | makeHTMLRequest, | ||
164 | makeGetRequest, | ||
165 | makeUploadRequest, | ||
166 | makePostBodyRequest, | ||
167 | makePutBodyRequest, | ||
168 | makeDeleteRequest, | ||
169 | updateAvatarRequest | ||
170 | } | ||
diff --git a/server/tests/utils/search/video-channels.ts b/server/tests/utils/search/video-channels.ts deleted file mode 100644 index 0532134ae..000000000 --- a/server/tests/utils/search/video-channels.ts +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | import { makeGetRequest } from '../requests/requests' | ||
2 | |||
3 | function searchVideoChannel (url: string, search: string, token?: string, statusCodeExpected = 200) { | ||
4 | const path = '/api/v1/search/video-channels' | ||
5 | |||
6 | return makeGetRequest({ | ||
7 | url, | ||
8 | path, | ||
9 | query: { | ||
10 | sort: '-createdAt', | ||
11 | search | ||
12 | }, | ||
13 | token, | ||
14 | statusCodeExpected | ||
15 | }) | ||
16 | } | ||
17 | |||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | export { | ||
21 | searchVideoChannel | ||
22 | } | ||
diff --git a/server/tests/utils/search/videos.ts b/server/tests/utils/search/videos.ts deleted file mode 100644 index 3a0c10e42..000000000 --- a/server/tests/utils/search/videos.ts +++ /dev/null | |||
@@ -1,77 +0,0 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as request from 'supertest' | ||
4 | import { VideosSearchQuery } from '../../../../shared/models/search' | ||
5 | import { immutableAssign } from '..' | ||
6 | |||
7 | function searchVideo (url: string, search: string) { | ||
8 | const path = '/api/v1/search/videos' | ||
9 | const req = request(url) | ||
10 | .get(path) | ||
11 | .query({ sort: '-publishedAt', search }) | ||
12 | .set('Accept', 'application/json') | ||
13 | |||
14 | return req.expect(200) | ||
15 | .expect('Content-Type', /json/) | ||
16 | } | ||
17 | |||
18 | function searchVideoWithToken (url: string, search: string, token: string, query: { nsfw?: boolean } = {}) { | ||
19 | const path = '/api/v1/search/videos' | ||
20 | const req = request(url) | ||
21 | .get(path) | ||
22 | .set('Authorization', 'Bearer ' + token) | ||
23 | .query(immutableAssign(query, { sort: '-publishedAt', search })) | ||
24 | .set('Accept', 'application/json') | ||
25 | |||
26 | return req.expect(200) | ||
27 | .expect('Content-Type', /json/) | ||
28 | } | ||
29 | |||
30 | function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) { | ||
31 | const path = '/api/v1/search/videos' | ||
32 | |||
33 | const req = request(url) | ||
34 | .get(path) | ||
35 | .query({ start }) | ||
36 | .query({ search }) | ||
37 | .query({ count }) | ||
38 | |||
39 | if (sort) req.query({ sort }) | ||
40 | |||
41 | return req.set('Accept', 'application/json') | ||
42 | .expect(200) | ||
43 | .expect('Content-Type', /json/) | ||
44 | } | ||
45 | |||
46 | function searchVideoWithSort (url: string, search: string, sort: string) { | ||
47 | const path = '/api/v1/search/videos' | ||
48 | |||
49 | return request(url) | ||
50 | .get(path) | ||
51 | .query({ search }) | ||
52 | .query({ sort }) | ||
53 | .set('Accept', 'application/json') | ||
54 | .expect(200) | ||
55 | .expect('Content-Type', /json/) | ||
56 | } | ||
57 | |||
58 | function advancedVideosSearch (url: string, options: VideosSearchQuery) { | ||
59 | const path = '/api/v1/search/videos' | ||
60 | |||
61 | return request(url) | ||
62 | .get(path) | ||
63 | .query(options) | ||
64 | .set('Accept', 'application/json') | ||
65 | .expect(200) | ||
66 | .expect('Content-Type', /json/) | ||
67 | } | ||
68 | |||
69 | // --------------------------------------------------------------------------- | ||
70 | |||
71 | export { | ||
72 | searchVideo, | ||
73 | advancedVideosSearch, | ||
74 | searchVideoWithToken, | ||
75 | searchVideoWithPagination, | ||
76 | searchVideoWithSort | ||
77 | } | ||
diff --git a/server/tests/utils/server/activitypub.ts b/server/tests/utils/server/activitypub.ts deleted file mode 100644 index cf3c1c3b3..000000000 --- a/server/tests/utils/server/activitypub.ts +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | |||
3 | function makeActivityPubGetRequest (url: string, path: string) { | ||
4 | return request(url) | ||
5 | .get(path) | ||
6 | .set('Accept', 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8') | ||
7 | .expect(200) | ||
8 | .expect('Content-Type', /json/) | ||
9 | } | ||
10 | |||
11 | // --------------------------------------------------------------------------- | ||
12 | |||
13 | export { | ||
14 | makeActivityPubGetRequest | ||
15 | } | ||
diff --git a/server/tests/utils/server/clients.ts b/server/tests/utils/server/clients.ts deleted file mode 100644 index 273aac747..000000000 --- a/server/tests/utils/server/clients.ts +++ /dev/null | |||
@@ -1,19 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import * as urlUtil from 'url' | ||
3 | |||
4 | function getClient (url: string) { | ||
5 | const path = '/api/v1/oauth-clients/local' | ||
6 | |||
7 | return request(url) | ||
8 | .get(path) | ||
9 | .set('Host', urlUtil.parse(url).host) | ||
10 | .set('Accept', 'application/json') | ||
11 | .expect(200) | ||
12 | .expect('Content-Type', /json/) | ||
13 | } | ||
14 | |||
15 | // --------------------------------------------------------------------------- | ||
16 | |||
17 | export { | ||
18 | getClient | ||
19 | } | ||
diff --git a/server/tests/utils/server/config.ts b/server/tests/utils/server/config.ts deleted file mode 100644 index b85e02ab7..000000000 --- a/server/tests/utils/server/config.ts +++ /dev/null | |||
@@ -1,135 +0,0 @@ | |||
1 | import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../' | ||
2 | import { CustomConfig } from '../../../../shared/models/server/custom-config.model' | ||
3 | |||
4 | function getConfig (url: string) { | ||
5 | const path = '/api/v1/config' | ||
6 | |||
7 | return makeGetRequest({ | ||
8 | url, | ||
9 | path, | ||
10 | statusCodeExpected: 200 | ||
11 | }) | ||
12 | } | ||
13 | |||
14 | function getAbout (url: string) { | ||
15 | const path = '/api/v1/config/about' | ||
16 | |||
17 | return makeGetRequest({ | ||
18 | url, | ||
19 | path, | ||
20 | statusCodeExpected: 200 | ||
21 | }) | ||
22 | } | ||
23 | |||
24 | function getCustomConfig (url: string, token: string, statusCodeExpected = 200) { | ||
25 | const path = '/api/v1/config/custom' | ||
26 | |||
27 | return makeGetRequest({ | ||
28 | url, | ||
29 | token, | ||
30 | path, | ||
31 | statusCodeExpected | ||
32 | }) | ||
33 | } | ||
34 | |||
35 | function updateCustomConfig (url: string, token: string, newCustomConfig: CustomConfig, statusCodeExpected = 200) { | ||
36 | const path = '/api/v1/config/custom' | ||
37 | |||
38 | return makePutBodyRequest({ | ||
39 | url, | ||
40 | token, | ||
41 | path, | ||
42 | fields: newCustomConfig, | ||
43 | statusCodeExpected | ||
44 | }) | ||
45 | } | ||
46 | |||
47 | function updateCustomSubConfig (url: string, token: string, newConfig: any) { | ||
48 | const updateParams: CustomConfig = { | ||
49 | instance: { | ||
50 | name: 'PeerTube updated', | ||
51 | shortDescription: 'my short description', | ||
52 | description: 'my super description', | ||
53 | terms: 'my super terms', | ||
54 | defaultClientRoute: '/videos/recently-added', | ||
55 | defaultNSFWPolicy: 'blur', | ||
56 | customizations: { | ||
57 | javascript: 'alert("coucou")', | ||
58 | css: 'body { background-color: red; }' | ||
59 | } | ||
60 | }, | ||
61 | services: { | ||
62 | twitter: { | ||
63 | username: '@MySuperUsername', | ||
64 | whitelisted: true | ||
65 | } | ||
66 | }, | ||
67 | cache: { | ||
68 | previews: { | ||
69 | size: 2 | ||
70 | }, | ||
71 | captions: { | ||
72 | size: 3 | ||
73 | } | ||
74 | }, | ||
75 | signup: { | ||
76 | enabled: false, | ||
77 | limit: 5, | ||
78 | requiresEmailVerification: false | ||
79 | }, | ||
80 | admin: { | ||
81 | email: 'superadmin1@example.com' | ||
82 | }, | ||
83 | user: { | ||
84 | videoQuota: 5242881, | ||
85 | videoQuotaDaily: 318742 | ||
86 | }, | ||
87 | transcoding: { | ||
88 | enabled: true, | ||
89 | threads: 1, | ||
90 | resolutions: { | ||
91 | '240p': false, | ||
92 | '360p': true, | ||
93 | '480p': true, | ||
94 | '720p': false, | ||
95 | '1080p': false | ||
96 | } | ||
97 | }, | ||
98 | import: { | ||
99 | videos: { | ||
100 | http: { | ||
101 | enabled: false | ||
102 | }, | ||
103 | torrent: { | ||
104 | enabled: false | ||
105 | } | ||
106 | } | ||
107 | } | ||
108 | } | ||
109 | |||
110 | Object.assign(updateParams, newConfig) | ||
111 | |||
112 | return updateCustomConfig(url, token, updateParams) | ||
113 | } | ||
114 | |||
115 | function deleteCustomConfig (url: string, token: string, statusCodeExpected = 200) { | ||
116 | const path = '/api/v1/config/custom' | ||
117 | |||
118 | return makeDeleteRequest({ | ||
119 | url, | ||
120 | token, | ||
121 | path, | ||
122 | statusCodeExpected | ||
123 | }) | ||
124 | } | ||
125 | |||
126 | // --------------------------------------------------------------------------- | ||
127 | |||
128 | export { | ||
129 | getConfig, | ||
130 | getCustomConfig, | ||
131 | updateCustomConfig, | ||
132 | getAbout, | ||
133 | deleteCustomConfig, | ||
134 | updateCustomSubConfig | ||
135 | } | ||
diff --git a/server/tests/utils/server/follows.ts b/server/tests/utils/server/follows.ts deleted file mode 100644 index 8a65a958b..000000000 --- a/server/tests/utils/server/follows.ts +++ /dev/null | |||
@@ -1,77 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import { ServerInfo } from './servers' | ||
3 | import { waitJobs } from './jobs' | ||
4 | |||
5 | function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string) { | ||
6 | const path = '/api/v1/server/followers' | ||
7 | |||
8 | return request(url) | ||
9 | .get(path) | ||
10 | .query({ start }) | ||
11 | .query({ count }) | ||
12 | .query({ sort }) | ||
13 | .set('Accept', 'application/json') | ||
14 | .expect(200) | ||
15 | .expect('Content-Type', /json/) | ||
16 | } | ||
17 | |||
18 | function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string) { | ||
19 | const path = '/api/v1/server/following' | ||
20 | |||
21 | return request(url) | ||
22 | .get(path) | ||
23 | .query({ start }) | ||
24 | .query({ count }) | ||
25 | .query({ sort }) | ||
26 | .set('Accept', 'application/json') | ||
27 | .expect(200) | ||
28 | .expect('Content-Type', /json/) | ||
29 | } | ||
30 | |||
31 | async function follow (follower: string, following: string[], accessToken: string, expectedStatus = 204) { | ||
32 | const path = '/api/v1/server/following' | ||
33 | |||
34 | const followingHosts = following.map(f => f.replace(/^http:\/\//, '')) | ||
35 | const res = await request(follower) | ||
36 | .post(path) | ||
37 | .set('Accept', 'application/json') | ||
38 | .set('Authorization', 'Bearer ' + accessToken) | ||
39 | .send({ 'hosts': followingHosts }) | ||
40 | .expect(expectedStatus) | ||
41 | |||
42 | return res | ||
43 | } | ||
44 | |||
45 | async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) { | ||
46 | const path = '/api/v1/server/following/' + target.host | ||
47 | |||
48 | const res = await request(url) | ||
49 | .delete(path) | ||
50 | .set('Accept', 'application/json') | ||
51 | .set('Authorization', 'Bearer ' + accessToken) | ||
52 | .expect(expectedStatus) | ||
53 | |||
54 | return res | ||
55 | } | ||
56 | |||
57 | async function doubleFollow (server1: ServerInfo, server2: ServerInfo) { | ||
58 | await Promise.all([ | ||
59 | follow(server1.url, [ server2.url ], server1.accessToken), | ||
60 | follow(server2.url, [ server1.url ], server2.accessToken) | ||
61 | ]) | ||
62 | |||
63 | // Wait request propagation | ||
64 | await waitJobs([ server1, server2 ]) | ||
65 | |||
66 | return true | ||
67 | } | ||
68 | |||
69 | // --------------------------------------------------------------------------- | ||
70 | |||
71 | export { | ||
72 | getFollowersListPaginationAndSort, | ||
73 | getFollowingListPaginationAndSort, | ||
74 | unfollow, | ||
75 | follow, | ||
76 | doubleFollow | ||
77 | } | ||
diff --git a/server/tests/utils/server/jobs.ts b/server/tests/utils/server/jobs.ts deleted file mode 100644 index 4c02cace5..000000000 --- a/server/tests/utils/server/jobs.ts +++ /dev/null | |||
@@ -1,77 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import { Job, JobState } from '../../../../shared/models' | ||
3 | import { ServerInfo, wait } from '../index' | ||
4 | |||
5 | function getJobsList (url: string, accessToken: string, state: JobState) { | ||
6 | const path = '/api/v1/jobs/' + state | ||
7 | |||
8 | return request(url) | ||
9 | .get(path) | ||
10 | .set('Accept', 'application/json') | ||
11 | .set('Authorization', 'Bearer ' + accessToken) | ||
12 | .expect(200) | ||
13 | .expect('Content-Type', /json/) | ||
14 | } | ||
15 | |||
16 | function getJobsListPaginationAndSort (url: string, accessToken: string, state: JobState, start: number, count: number, sort: string) { | ||
17 | const path = '/api/v1/jobs/' + state | ||
18 | |||
19 | return request(url) | ||
20 | .get(path) | ||
21 | .query({ start }) | ||
22 | .query({ count }) | ||
23 | .query({ sort }) | ||
24 | .set('Accept', 'application/json') | ||
25 | .set('Authorization', 'Bearer ' + accessToken) | ||
26 | .expect(200) | ||
27 | .expect('Content-Type', /json/) | ||
28 | } | ||
29 | |||
30 | async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { | ||
31 | let servers: ServerInfo[] | ||
32 | |||
33 | if (Array.isArray(serversArg) === false) servers = [ serversArg as ServerInfo ] | ||
34 | else servers = serversArg as ServerInfo[] | ||
35 | |||
36 | const states: JobState[] = [ 'waiting', 'active', 'delayed' ] | ||
37 | const tasks: Promise<any>[] = [] | ||
38 | let pendingRequests: boolean | ||
39 | |||
40 | do { | ||
41 | pendingRequests = false | ||
42 | |||
43 | // Check if each server has pending request | ||
44 | for (const server of servers) { | ||
45 | for (const state of states) { | ||
46 | const p = getJobsListPaginationAndSort(server.url, server.accessToken, state, 0, 10, '-createdAt') | ||
47 | .then(res => res.body.data) | ||
48 | .then((jobs: Job[]) => jobs.filter(j => j.type !== 'videos-views')) | ||
49 | .then(jobs => { | ||
50 | if (jobs.length !== 0) pendingRequests = true | ||
51 | }) | ||
52 | tasks.push(p) | ||
53 | } | ||
54 | } | ||
55 | |||
56 | await Promise.all(tasks) | ||
57 | |||
58 | // Retry, in case of new jobs were created | ||
59 | if (pendingRequests === false) { | ||
60 | await wait(1000) | ||
61 | |||
62 | await Promise.all(tasks) | ||
63 | } | ||
64 | |||
65 | if (pendingRequests) { | ||
66 | await wait(1000) | ||
67 | } | ||
68 | } while (pendingRequests) | ||
69 | } | ||
70 | |||
71 | // --------------------------------------------------------------------------- | ||
72 | |||
73 | export { | ||
74 | getJobsList, | ||
75 | waitJobs, | ||
76 | getJobsListPaginationAndSort | ||
77 | } | ||
diff --git a/server/tests/utils/server/redundancy.ts b/server/tests/utils/server/redundancy.ts deleted file mode 100644 index c39ff2c8b..000000000 --- a/server/tests/utils/server/redundancy.ts +++ /dev/null | |||
@@ -1,17 +0,0 @@ | |||
1 | import { makePutBodyRequest } from '../requests/requests' | ||
2 | |||
3 | async function updateRedundancy (url: string, accessToken: string, host: string, redundancyAllowed: boolean, expectedStatus = 204) { | ||
4 | const path = '/api/v1/server/redundancy/' + host | ||
5 | |||
6 | return makePutBodyRequest({ | ||
7 | url, | ||
8 | path, | ||
9 | token: accessToken, | ||
10 | fields: { redundancyAllowed }, | ||
11 | statusCodeExpected: expectedStatus | ||
12 | }) | ||
13 | } | ||
14 | |||
15 | export { | ||
16 | updateRedundancy | ||
17 | } | ||
diff --git a/server/tests/utils/server/servers.ts b/server/tests/utils/server/servers.ts deleted file mode 100644 index 3c946db27..000000000 --- a/server/tests/utils/server/servers.ts +++ /dev/null | |||
@@ -1,185 +0,0 @@ | |||
1 | import { ChildProcess, exec, fork } from 'child_process' | ||
2 | import { join } from 'path' | ||
3 | import { root, wait } from '../miscs/miscs' | ||
4 | import { readFile } from 'fs-extra' | ||
5 | |||
6 | interface ServerInfo { | ||
7 | app: ChildProcess, | ||
8 | url: string | ||
9 | host: string | ||
10 | serverNumber: number | ||
11 | |||
12 | client: { | ||
13 | id: string, | ||
14 | secret: string | ||
15 | } | ||
16 | |||
17 | user: { | ||
18 | username: string, | ||
19 | password: string, | ||
20 | email?: string | ||
21 | } | ||
22 | |||
23 | accessToken?: string | ||
24 | |||
25 | video?: { | ||
26 | id: number | ||
27 | uuid: string | ||
28 | name: string | ||
29 | account: { | ||
30 | name: string | ||
31 | } | ||
32 | } | ||
33 | |||
34 | remoteVideo?: { | ||
35 | id: number | ||
36 | uuid: string | ||
37 | } | ||
38 | } | ||
39 | |||
40 | function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) { | ||
41 | let apps = [] | ||
42 | let i = 0 | ||
43 | |||
44 | return new Promise<ServerInfo[]>(res => { | ||
45 | function anotherServerDone (serverNumber, app) { | ||
46 | apps[serverNumber - 1] = app | ||
47 | i++ | ||
48 | if (i === totalServers) { | ||
49 | return res(apps) | ||
50 | } | ||
51 | } | ||
52 | |||
53 | flushTests() | ||
54 | .then(() => { | ||
55 | for (let j = 1; j <= totalServers; j++) { | ||
56 | runServer(j, configOverride).then(app => anotherServerDone(j, app)) | ||
57 | } | ||
58 | }) | ||
59 | }) | ||
60 | } | ||
61 | |||
62 | function flushTests () { | ||
63 | return new Promise<void>((res, rej) => { | ||
64 | return exec('npm run clean:server:test', err => { | ||
65 | if (err) return rej(err) | ||
66 | |||
67 | return res() | ||
68 | }) | ||
69 | }) | ||
70 | } | ||
71 | |||
72 | function runServer (serverNumber: number, configOverride?: Object) { | ||
73 | const server: ServerInfo = { | ||
74 | app: null, | ||
75 | serverNumber: serverNumber, | ||
76 | url: `http://localhost:${9000 + serverNumber}`, | ||
77 | host: `localhost:${9000 + serverNumber}`, | ||
78 | client: { | ||
79 | id: null, | ||
80 | secret: null | ||
81 | }, | ||
82 | user: { | ||
83 | username: null, | ||
84 | password: null | ||
85 | } | ||
86 | } | ||
87 | |||
88 | // These actions are async so we need to be sure that they have both been done | ||
89 | const serverRunString = { | ||
90 | 'Server listening': false | ||
91 | } | ||
92 | const key = 'Database peertube_test' + serverNumber + ' is ready' | ||
93 | serverRunString[key] = false | ||
94 | |||
95 | const regexps = { | ||
96 | client_id: 'Client id: (.+)', | ||
97 | client_secret: 'Client secret: (.+)', | ||
98 | user_username: 'Username: (.+)', | ||
99 | user_password: 'User password: (.+)' | ||
100 | } | ||
101 | |||
102 | // Share the environment | ||
103 | const env = Object.create(process.env) | ||
104 | env['NODE_ENV'] = 'test' | ||
105 | env['NODE_APP_INSTANCE'] = serverNumber.toString() | ||
106 | |||
107 | if (configOverride !== undefined) { | ||
108 | env['NODE_CONFIG'] = JSON.stringify(configOverride) | ||
109 | } | ||
110 | |||
111 | const options = { | ||
112 | silent: true, | ||
113 | env: env, | ||
114 | detached: true | ||
115 | } | ||
116 | |||
117 | return new Promise<ServerInfo>(res => { | ||
118 | server.app = fork(join(__dirname, '..', '..', '..', '..', 'dist', 'server.js'), [], options) | ||
119 | server.app.stdout.on('data', function onStdout (data) { | ||
120 | let dontContinue = false | ||
121 | |||
122 | // Capture things if we want to | ||
123 | for (const key of Object.keys(regexps)) { | ||
124 | const regexp = regexps[key] | ||
125 | const matches = data.toString().match(regexp) | ||
126 | if (matches !== null) { | ||
127 | if (key === 'client_id') server.client.id = matches[1] | ||
128 | else if (key === 'client_secret') server.client.secret = matches[1] | ||
129 | else if (key === 'user_username') server.user.username = matches[1] | ||
130 | else if (key === 'user_password') server.user.password = matches[1] | ||
131 | } | ||
132 | } | ||
133 | |||
134 | // Check if all required sentences are here | ||
135 | for (const key of Object.keys(serverRunString)) { | ||
136 | if (data.toString().indexOf(key) !== -1) serverRunString[key] = true | ||
137 | if (serverRunString[key] === false) dontContinue = true | ||
138 | } | ||
139 | |||
140 | // If no, there is maybe one thing not already initialized (client/user credentials generation...) | ||
141 | if (dontContinue === true) return | ||
142 | |||
143 | server.app.stdout.removeListener('data', onStdout) | ||
144 | res(server) | ||
145 | }) | ||
146 | }) | ||
147 | } | ||
148 | |||
149 | async function reRunServer (server: ServerInfo, configOverride?: any) { | ||
150 | const newServer = await runServer(server.serverNumber, configOverride) | ||
151 | server.app = newServer.app | ||
152 | |||
153 | return server | ||
154 | } | ||
155 | |||
156 | function killallServers (servers: ServerInfo[]) { | ||
157 | for (const server of servers) { | ||
158 | process.kill(-server.app.pid) | ||
159 | } | ||
160 | } | ||
161 | |||
162 | async function waitUntilLog (server: ServerInfo, str: string, count = 1) { | ||
163 | const logfile = join(root(), 'test' + server.serverNumber, 'logs/peertube.log') | ||
164 | |||
165 | while (true) { | ||
166 | const buf = await readFile(logfile) | ||
167 | |||
168 | const matches = buf.toString().match(new RegExp(str, 'g')) | ||
169 | if (matches && matches.length === count) return | ||
170 | |||
171 | await wait(1000) | ||
172 | } | ||
173 | } | ||
174 | |||
175 | // --------------------------------------------------------------------------- | ||
176 | |||
177 | export { | ||
178 | ServerInfo, | ||
179 | flushAndRunMultipleServers, | ||
180 | flushTests, | ||
181 | runServer, | ||
182 | killallServers, | ||
183 | reRunServer, | ||
184 | waitUntilLog | ||
185 | } | ||
diff --git a/server/tests/utils/server/stats.ts b/server/tests/utils/server/stats.ts deleted file mode 100644 index 01989d952..000000000 --- a/server/tests/utils/server/stats.ts +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | import { makeGetRequest } from '../' | ||
2 | |||
3 | function getStats (url: string, useCache = false) { | ||
4 | const path = '/api/v1/server/stats' | ||
5 | |||
6 | const query = { | ||
7 | t: useCache ? undefined : new Date().getTime() | ||
8 | } | ||
9 | |||
10 | return makeGetRequest({ | ||
11 | url, | ||
12 | path, | ||
13 | query, | ||
14 | statusCodeExpected: 200 | ||
15 | }) | ||
16 | } | ||
17 | |||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | export { | ||
21 | getStats | ||
22 | } | ||
diff --git a/server/tests/utils/users/accounts.ts b/server/tests/utils/users/accounts.ts deleted file mode 100644 index f82b8d906..000000000 --- a/server/tests/utils/users/accounts.ts +++ /dev/null | |||
@@ -1,63 +0,0 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { existsSync, readdir } from 'fs-extra' | ||
5 | import { join } from 'path' | ||
6 | import { Account } from '../../../../shared/models/actors' | ||
7 | import { root } from '../index' | ||
8 | import { makeGetRequest } from '../requests/requests' | ||
9 | |||
10 | function getAccountsList (url: string, sort = '-createdAt', statusCodeExpected = 200) { | ||
11 | const path = '/api/v1/accounts' | ||
12 | |||
13 | return makeGetRequest({ | ||
14 | url, | ||
15 | query: { sort }, | ||
16 | path, | ||
17 | statusCodeExpected | ||
18 | }) | ||
19 | } | ||
20 | |||
21 | function getAccount (url: string, accountName: string, statusCodeExpected = 200) { | ||
22 | const path = '/api/v1/accounts/' + accountName | ||
23 | |||
24 | return makeGetRequest({ | ||
25 | url, | ||
26 | path, | ||
27 | statusCodeExpected | ||
28 | }) | ||
29 | } | ||
30 | |||
31 | async function expectAccountFollows (url: string, nameWithDomain: string, followersCount: number, followingCount: number) { | ||
32 | const res = await getAccountsList(url) | ||
33 | const account = res.body.data.find((a: Account) => a.name + '@' + a.host === nameWithDomain) | ||
34 | |||
35 | const message = `${nameWithDomain} on ${url}` | ||
36 | expect(account.followersCount).to.equal(followersCount, message) | ||
37 | expect(account.followingCount).to.equal(followingCount, message) | ||
38 | } | ||
39 | |||
40 | async function checkActorFilesWereRemoved (actorUUID: string, serverNumber: number) { | ||
41 | const testDirectory = 'test' + serverNumber | ||
42 | |||
43 | for (const directory of [ 'avatars' ]) { | ||
44 | const directoryPath = join(root(), testDirectory, directory) | ||
45 | |||
46 | const directoryExists = existsSync(directoryPath) | ||
47 | expect(directoryExists).to.be.true | ||
48 | |||
49 | const files = await readdir(directoryPath) | ||
50 | for (const file of files) { | ||
51 | expect(file).to.not.contain(actorUUID) | ||
52 | } | ||
53 | } | ||
54 | } | ||
55 | |||
56 | // --------------------------------------------------------------------------- | ||
57 | |||
58 | export { | ||
59 | getAccount, | ||
60 | expectAccountFollows, | ||
61 | getAccountsList, | ||
62 | checkActorFilesWereRemoved | ||
63 | } | ||
diff --git a/server/tests/utils/users/login.ts b/server/tests/utils/users/login.ts deleted file mode 100644 index ddeb9df2a..000000000 --- a/server/tests/utils/users/login.ts +++ /dev/null | |||
@@ -1,62 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | |||
3 | import { ServerInfo } from '../server/servers' | ||
4 | |||
5 | type Client = { id: string, secret: string } | ||
6 | type User = { username: string, password: string } | ||
7 | type Server = { url: string, client: Client, user: User } | ||
8 | |||
9 | function login (url: string, client: Client, user: User, expectedStatus = 200) { | ||
10 | const path = '/api/v1/users/token' | ||
11 | |||
12 | const body = { | ||
13 | client_id: client.id, | ||
14 | client_secret: client.secret, | ||
15 | username: user.username, | ||
16 | password: user.password, | ||
17 | response_type: 'code', | ||
18 | grant_type: 'password', | ||
19 | scope: 'upload' | ||
20 | } | ||
21 | |||
22 | return request(url) | ||
23 | .post(path) | ||
24 | .type('form') | ||
25 | .send(body) | ||
26 | .expect(expectedStatus) | ||
27 | } | ||
28 | |||
29 | async function serverLogin (server: Server) { | ||
30 | const res = await login(server.url, server.client, server.user, 200) | ||
31 | |||
32 | return res.body.access_token as string | ||
33 | } | ||
34 | |||
35 | async function userLogin (server: Server, user: User, expectedStatus = 200) { | ||
36 | const res = await login(server.url, server.client, user, expectedStatus) | ||
37 | |||
38 | return res.body.access_token as string | ||
39 | } | ||
40 | |||
41 | function setAccessTokensToServers (servers: ServerInfo[]) { | ||
42 | const tasks: Promise<any>[] = [] | ||
43 | |||
44 | for (const server of servers) { | ||
45 | const p = serverLogin(server).then(t => server.accessToken = t) | ||
46 | tasks.push(p) | ||
47 | } | ||
48 | |||
49 | return Promise.all(tasks) | ||
50 | } | ||
51 | |||
52 | // --------------------------------------------------------------------------- | ||
53 | |||
54 | export { | ||
55 | login, | ||
56 | serverLogin, | ||
57 | userLogin, | ||
58 | setAccessTokensToServers, | ||
59 | Server, | ||
60 | Client, | ||
61 | User | ||
62 | } | ||
diff --git a/server/tests/utils/users/user-subscriptions.ts b/server/tests/utils/users/user-subscriptions.ts deleted file mode 100644 index b0e7da7cc..000000000 --- a/server/tests/utils/users/user-subscriptions.ts +++ /dev/null | |||
@@ -1,82 +0,0 @@ | |||
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 getUserSubscription (url: string, token: string, uri: string, statusCodeExpected = 200) { | ||
40 | const path = '/api/v1/users/me/subscriptions/' + uri | ||
41 | |||
42 | return makeGetRequest({ | ||
43 | url, | ||
44 | path, | ||
45 | token, | ||
46 | statusCodeExpected | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | function removeUserSubscription (url: string, token: string, uri: string, statusCodeExpected = 204) { | ||
51 | const path = '/api/v1/users/me/subscriptions/' + uri | ||
52 | |||
53 | return makeDeleteRequest({ | ||
54 | url, | ||
55 | path, | ||
56 | token, | ||
57 | statusCodeExpected | ||
58 | }) | ||
59 | } | ||
60 | |||
61 | function areSubscriptionsExist (url: string, token: string, uris: string[], statusCodeExpected = 200) { | ||
62 | const path = '/api/v1/users/me/subscriptions/exist' | ||
63 | |||
64 | return makeGetRequest({ | ||
65 | url, | ||
66 | path, | ||
67 | query: { 'uris[]': uris }, | ||
68 | token, | ||
69 | statusCodeExpected | ||
70 | }) | ||
71 | } | ||
72 | |||
73 | // --------------------------------------------------------------------------- | ||
74 | |||
75 | export { | ||
76 | areSubscriptionsExist, | ||
77 | addUserSubscription, | ||
78 | listUserSubscriptions, | ||
79 | getUserSubscription, | ||
80 | listUserSubscriptionVideos, | ||
81 | removeUserSubscription | ||
82 | } | ||
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts deleted file mode 100644 index 41d8ce265..000000000 --- a/server/tests/utils/users/users.ts +++ /dev/null | |||
@@ -1,295 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../' | ||
3 | |||
4 | import { UserRole } from '../../../../shared/index' | ||
5 | import { NSFWPolicyType } from '../../../../shared/models/videos/nsfw-policy.type' | ||
6 | |||
7 | function createUser ( | ||
8 | url: string, | ||
9 | accessToken: string, | ||
10 | username: string, | ||
11 | password: string, | ||
12 | videoQuota = 1000000, | ||
13 | videoQuotaDaily = -1, | ||
14 | role: UserRole = UserRole.USER, | ||
15 | specialStatus = 200 | ||
16 | ) { | ||
17 | const path = '/api/v1/users' | ||
18 | const body = { | ||
19 | username, | ||
20 | password, | ||
21 | role, | ||
22 | email: username + '@example.com', | ||
23 | videoQuota, | ||
24 | videoQuotaDaily | ||
25 | } | ||
26 | |||
27 | return request(url) | ||
28 | .post(path) | ||
29 | .set('Accept', 'application/json') | ||
30 | .set('Authorization', 'Bearer ' + accessToken) | ||
31 | .send(body) | ||
32 | .expect(specialStatus) | ||
33 | } | ||
34 | |||
35 | function registerUser (url: string, username: string, password: string, specialStatus = 204) { | ||
36 | const path = '/api/v1/users/register' | ||
37 | const body = { | ||
38 | username, | ||
39 | password, | ||
40 | email: username + '@example.com' | ||
41 | } | ||
42 | |||
43 | return request(url) | ||
44 | .post(path) | ||
45 | .set('Accept', 'application/json') | ||
46 | .send(body) | ||
47 | .expect(specialStatus) | ||
48 | } | ||
49 | |||
50 | function getMyUserInformation (url: string, accessToken: string, specialStatus = 200) { | ||
51 | const path = '/api/v1/users/me' | ||
52 | |||
53 | return request(url) | ||
54 | .get(path) | ||
55 | .set('Accept', 'application/json') | ||
56 | .set('Authorization', 'Bearer ' + accessToken) | ||
57 | .expect(specialStatus) | ||
58 | .expect('Content-Type', /json/) | ||
59 | } | ||
60 | |||
61 | function deleteMe (url: string, accessToken: string, specialStatus = 204) { | ||
62 | const path = '/api/v1/users/me' | ||
63 | |||
64 | return request(url) | ||
65 | .delete(path) | ||
66 | .set('Accept', 'application/json') | ||
67 | .set('Authorization', 'Bearer ' + accessToken) | ||
68 | .expect(specialStatus) | ||
69 | } | ||
70 | |||
71 | function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) { | ||
72 | const path = '/api/v1/users/me/video-quota-used' | ||
73 | |||
74 | return request(url) | ||
75 | .get(path) | ||
76 | .set('Accept', 'application/json') | ||
77 | .set('Authorization', 'Bearer ' + accessToken) | ||
78 | .expect(specialStatus) | ||
79 | .expect('Content-Type', /json/) | ||
80 | } | ||
81 | |||
82 | function getUserInformation (url: string, accessToken: string, userId: number) { | ||
83 | const path = '/api/v1/users/' + userId | ||
84 | |||
85 | return request(url) | ||
86 | .get(path) | ||
87 | .set('Accept', 'application/json') | ||
88 | .set('Authorization', 'Bearer ' + accessToken) | ||
89 | .expect(200) | ||
90 | .expect('Content-Type', /json/) | ||
91 | } | ||
92 | |||
93 | function getMyUserVideoRating (url: string, accessToken: string, videoId: number | string, specialStatus = 200) { | ||
94 | const path = '/api/v1/users/me/videos/' + videoId + '/rating' | ||
95 | |||
96 | return request(url) | ||
97 | .get(path) | ||
98 | .set('Accept', 'application/json') | ||
99 | .set('Authorization', 'Bearer ' + accessToken) | ||
100 | .expect(specialStatus) | ||
101 | .expect('Content-Type', /json/) | ||
102 | } | ||
103 | |||
104 | function getUsersList (url: string, accessToken: string) { | ||
105 | const path = '/api/v1/users' | ||
106 | |||
107 | return request(url) | ||
108 | .get(path) | ||
109 | .set('Accept', 'application/json') | ||
110 | .set('Authorization', 'Bearer ' + accessToken) | ||
111 | .expect(200) | ||
112 | .expect('Content-Type', /json/) | ||
113 | } | ||
114 | |||
115 | function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string) { | ||
116 | const path = '/api/v1/users' | ||
117 | |||
118 | return request(url) | ||
119 | .get(path) | ||
120 | .query({ start }) | ||
121 | .query({ count }) | ||
122 | .query({ sort }) | ||
123 | .set('Accept', 'application/json') | ||
124 | .set('Authorization', 'Bearer ' + accessToken) | ||
125 | .expect(200) | ||
126 | .expect('Content-Type', /json/) | ||
127 | } | ||
128 | |||
129 | function removeUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) { | ||
130 | const path = '/api/v1/users' | ||
131 | |||
132 | return request(url) | ||
133 | .delete(path + '/' + userId) | ||
134 | .set('Accept', 'application/json') | ||
135 | .set('Authorization', 'Bearer ' + accessToken) | ||
136 | .expect(expectedStatus) | ||
137 | } | ||
138 | |||
139 | function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204, reason?: string) { | ||
140 | const path = '/api/v1/users' | ||
141 | let body: any | ||
142 | if (reason) body = { reason } | ||
143 | |||
144 | return request(url) | ||
145 | .post(path + '/' + userId + '/block') | ||
146 | .send(body) | ||
147 | .set('Accept', 'application/json') | ||
148 | .set('Authorization', 'Bearer ' + accessToken) | ||
149 | .expect(expectedStatus) | ||
150 | } | ||
151 | |||
152 | function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) { | ||
153 | const path = '/api/v1/users' | ||
154 | |||
155 | return request(url) | ||
156 | .post(path + '/' + userId + '/unblock') | ||
157 | .set('Accept', 'application/json') | ||
158 | .set('Authorization', 'Bearer ' + accessToken) | ||
159 | .expect(expectedStatus) | ||
160 | } | ||
161 | |||
162 | function updateMyUser (options: { | ||
163 | url: string | ||
164 | accessToken: string, | ||
165 | currentPassword?: string, | ||
166 | newPassword?: string, | ||
167 | nsfwPolicy?: NSFWPolicyType, | ||
168 | email?: string, | ||
169 | autoPlayVideo?: boolean | ||
170 | displayName?: string, | ||
171 | description?: string | ||
172 | }) { | ||
173 | const path = '/api/v1/users/me' | ||
174 | |||
175 | const toSend = {} | ||
176 | if (options.currentPassword !== undefined && options.currentPassword !== null) toSend['currentPassword'] = options.currentPassword | ||
177 | if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword | ||
178 | if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend['nsfwPolicy'] = options.nsfwPolicy | ||
179 | if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo | ||
180 | if (options.email !== undefined && options.email !== null) toSend['email'] = options.email | ||
181 | if (options.description !== undefined && options.description !== null) toSend['description'] = options.description | ||
182 | if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName | ||
183 | |||
184 | return makePutBodyRequest({ | ||
185 | url: options.url, | ||
186 | path, | ||
187 | token: options.accessToken, | ||
188 | fields: toSend, | ||
189 | statusCodeExpected: 204 | ||
190 | }) | ||
191 | } | ||
192 | |||
193 | function updateMyAvatar (options: { | ||
194 | url: string, | ||
195 | accessToken: string, | ||
196 | fixture: string | ||
197 | }) { | ||
198 | const path = '/api/v1/users/me/avatar/pick' | ||
199 | |||
200 | return updateAvatarRequest(Object.assign(options, { path })) | ||
201 | } | ||
202 | |||
203 | function updateUser (options: { | ||
204 | url: string | ||
205 | userId: number, | ||
206 | accessToken: string, | ||
207 | email?: string, | ||
208 | videoQuota?: number, | ||
209 | videoQuotaDaily?: number, | ||
210 | role?: UserRole | ||
211 | }) { | ||
212 | const path = '/api/v1/users/' + options.userId | ||
213 | |||
214 | const toSend = {} | ||
215 | if (options.email !== undefined && options.email !== null) toSend['email'] = options.email | ||
216 | if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota | ||
217 | if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily | ||
218 | if (options.role !== undefined && options.role !== null) toSend['role'] = options.role | ||
219 | |||
220 | return makePutBodyRequest({ | ||
221 | url: options.url, | ||
222 | path, | ||
223 | token: options.accessToken, | ||
224 | fields: toSend, | ||
225 | statusCodeExpected: 204 | ||
226 | }) | ||
227 | } | ||
228 | |||
229 | function askResetPassword (url: string, email: string) { | ||
230 | const path = '/api/v1/users/ask-reset-password' | ||
231 | |||
232 | return makePostBodyRequest({ | ||
233 | url, | ||
234 | path, | ||
235 | fields: { email }, | ||
236 | statusCodeExpected: 204 | ||
237 | }) | ||
238 | } | ||
239 | |||
240 | function resetPassword (url: string, userId: number, verificationString: string, password: string, statusCodeExpected = 204) { | ||
241 | const path = '/api/v1/users/' + userId + '/reset-password' | ||
242 | |||
243 | return makePostBodyRequest({ | ||
244 | url, | ||
245 | path, | ||
246 | fields: { password, verificationString }, | ||
247 | statusCodeExpected | ||
248 | }) | ||
249 | } | ||
250 | |||
251 | function askSendVerifyEmail (url: string, email: string) { | ||
252 | const path = '/api/v1/users/ask-send-verify-email' | ||
253 | |||
254 | return makePostBodyRequest({ | ||
255 | url, | ||
256 | path, | ||
257 | fields: { email }, | ||
258 | statusCodeExpected: 204 | ||
259 | }) | ||
260 | } | ||
261 | |||
262 | function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) { | ||
263 | const path = '/api/v1/users/' + userId + '/verify-email' | ||
264 | |||
265 | return makePostBodyRequest({ | ||
266 | url, | ||
267 | path, | ||
268 | fields: { verificationString }, | ||
269 | statusCodeExpected | ||
270 | }) | ||
271 | } | ||
272 | |||
273 | // --------------------------------------------------------------------------- | ||
274 | |||
275 | export { | ||
276 | createUser, | ||
277 | registerUser, | ||
278 | getMyUserInformation, | ||
279 | getMyUserVideoRating, | ||
280 | deleteMe, | ||
281 | getMyUserVideoQuotaUsed, | ||
282 | getUsersList, | ||
283 | getUsersListPaginationAndSort, | ||
284 | removeUser, | ||
285 | updateUser, | ||
286 | updateMyUser, | ||
287 | getUserInformation, | ||
288 | blockUser, | ||
289 | unblockUser, | ||
290 | askResetPassword, | ||
291 | resetPassword, | ||
292 | updateMyAvatar, | ||
293 | askSendVerifyEmail, | ||
294 | verifyEmail | ||
295 | } | ||
diff --git a/server/tests/utils/videos/services.ts b/server/tests/utils/videos/services.ts deleted file mode 100644 index 1a53dd4cf..000000000 --- a/server/tests/utils/videos/services.ts +++ /dev/null | |||
@@ -1,23 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | |||
3 | function getOEmbed (url: string, oembedUrl: string, format?: string, maxHeight?: number, maxWidth?: number) { | ||
4 | const path = '/services/oembed' | ||
5 | const query = { | ||
6 | url: oembedUrl, | ||
7 | format, | ||
8 | maxheight: maxHeight, | ||
9 | maxwidth: maxWidth | ||
10 | } | ||
11 | |||
12 | return request(url) | ||
13 | .get(path) | ||
14 | .query(query) | ||
15 | .set('Accept', 'application/json') | ||
16 | .expect(200) | ||
17 | } | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | export { | ||
22 | getOEmbed | ||
23 | } | ||
diff --git a/server/tests/utils/videos/video-abuses.ts b/server/tests/utils/videos/video-abuses.ts deleted file mode 100644 index 14907e6a0..000000000 --- a/server/tests/utils/videos/video-abuses.ts +++ /dev/null | |||
@@ -1,65 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import { VideoAbuseUpdate } from '../../../../shared/models/videos/abuse/video-abuse-update.model' | ||
3 | import { makeDeleteRequest, makePutBodyRequest } from '..' | ||
4 | |||
5 | function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 200) { | ||
6 | const path = '/api/v1/videos/' + videoId + '/abuse' | ||
7 | |||
8 | return request(url) | ||
9 | .post(path) | ||
10 | .set('Accept', 'application/json') | ||
11 | .set('Authorization', 'Bearer ' + token) | ||
12 | .send({ reason }) | ||
13 | .expect(specialStatus) | ||
14 | } | ||
15 | |||
16 | function getVideoAbusesList (url: string, token: string) { | ||
17 | const path = '/api/v1/videos/abuse' | ||
18 | |||
19 | return request(url) | ||
20 | .get(path) | ||
21 | .query({ sort: 'createdAt' }) | ||
22 | .set('Accept', 'application/json') | ||
23 | .set('Authorization', 'Bearer ' + token) | ||
24 | .expect(200) | ||
25 | .expect('Content-Type', /json/) | ||
26 | } | ||
27 | |||
28 | function updateVideoAbuse ( | ||
29 | url: string, | ||
30 | token: string, | ||
31 | videoId: string | number, | ||
32 | videoAbuseId: number, | ||
33 | body: VideoAbuseUpdate, | ||
34 | statusCodeExpected = 204 | ||
35 | ) { | ||
36 | const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId | ||
37 | |||
38 | return makePutBodyRequest({ | ||
39 | url, | ||
40 | token, | ||
41 | path, | ||
42 | fields: body, | ||
43 | statusCodeExpected | ||
44 | }) | ||
45 | } | ||
46 | |||
47 | function deleteVideoAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) { | ||
48 | const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId | ||
49 | |||
50 | return makeDeleteRequest({ | ||
51 | url, | ||
52 | token, | ||
53 | path, | ||
54 | statusCodeExpected | ||
55 | }) | ||
56 | } | ||
57 | |||
58 | // --------------------------------------------------------------------------- | ||
59 | |||
60 | export { | ||
61 | reportVideoAbuse, | ||
62 | getVideoAbusesList, | ||
63 | updateVideoAbuse, | ||
64 | deleteVideoAbuse | ||
65 | } | ||
diff --git a/server/tests/utils/videos/video-blacklist.ts b/server/tests/utils/videos/video-blacklist.ts deleted file mode 100644 index 2c176fde0..000000000 --- a/server/tests/utils/videos/video-blacklist.ts +++ /dev/null | |||
@@ -1,67 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | |||
3 | function addVideoToBlacklist (url: string, token: string, videoId: number | string, reason?: string, specialStatus = 204) { | ||
4 | const path = '/api/v1/videos/' + videoId + '/blacklist' | ||
5 | |||
6 | return request(url) | ||
7 | .post(path) | ||
8 | .send({ reason }) | ||
9 | .set('Accept', 'application/json') | ||
10 | .set('Authorization', 'Bearer ' + token) | ||
11 | .expect(specialStatus) | ||
12 | } | ||
13 | |||
14 | function updateVideoBlacklist (url: string, token: string, videoId: number, reason?: string, specialStatus = 204) { | ||
15 | const path = '/api/v1/videos/' + videoId + '/blacklist' | ||
16 | |||
17 | return request(url) | ||
18 | .put(path) | ||
19 | .send({ reason }) | ||
20 | .set('Accept', 'application/json') | ||
21 | .set('Authorization', 'Bearer ' + token) | ||
22 | .expect(specialStatus) | ||
23 | } | ||
24 | |||
25 | function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) { | ||
26 | const path = '/api/v1/videos/' + videoId + '/blacklist' | ||
27 | |||
28 | return request(url) | ||
29 | .delete(path) | ||
30 | .set('Accept', 'application/json') | ||
31 | .set('Authorization', 'Bearer ' + token) | ||
32 | .expect(specialStatus) | ||
33 | } | ||
34 | |||
35 | function getBlacklistedVideosList (url: string, token: string, specialStatus = 200) { | ||
36 | const path = '/api/v1/videos/blacklist/' | ||
37 | |||
38 | return request(url) | ||
39 | .get(path) | ||
40 | .query({ sort: 'createdAt' }) | ||
41 | .set('Accept', 'application/json') | ||
42 | .set('Authorization', 'Bearer ' + token) | ||
43 | .expect(specialStatus) | ||
44 | .expect('Content-Type', /json/) | ||
45 | } | ||
46 | |||
47 | function getSortedBlacklistedVideosList (url: string, token: string, sort: string, specialStatus = 200) { | ||
48 | const path = '/api/v1/videos/blacklist/' | ||
49 | |||
50 | return request(url) | ||
51 | .get(path) | ||
52 | .query({ sort: sort }) | ||
53 | .set('Accept', 'application/json') | ||
54 | .set('Authorization', 'Bearer ' + token) | ||
55 | .expect(specialStatus) | ||
56 | .expect('Content-Type', /json/) | ||
57 | } | ||
58 | |||
59 | // --------------------------------------------------------------------------- | ||
60 | |||
61 | export { | ||
62 | addVideoToBlacklist, | ||
63 | removeVideoFromBlacklist, | ||
64 | getBlacklistedVideosList, | ||
65 | getSortedBlacklistedVideosList, | ||
66 | updateVideoBlacklist | ||
67 | } | ||
diff --git a/server/tests/utils/videos/video-captions.ts b/server/tests/utils/videos/video-captions.ts deleted file mode 100644 index 41e52be07..000000000 --- a/server/tests/utils/videos/video-captions.ts +++ /dev/null | |||
@@ -1,71 +0,0 @@ | |||
1 | import { makeDeleteRequest, makeGetRequest } from '../' | ||
2 | import { buildAbsoluteFixturePath, makeUploadRequest } from '../index' | ||
3 | import * as request from 'supertest' | ||
4 | import * as chai from 'chai' | ||
5 | |||
6 | const expect = chai.expect | ||
7 | |||
8 | function createVideoCaption (args: { | ||
9 | url: string, | ||
10 | accessToken: string | ||
11 | videoId: string | number | ||
12 | language: string | ||
13 | fixture: string, | ||
14 | mimeType?: string, | ||
15 | statusCodeExpected?: number | ||
16 | }) { | ||
17 | const path = '/api/v1/videos/' + args.videoId + '/captions/' + args.language | ||
18 | |||
19 | const captionfile = buildAbsoluteFixturePath(args.fixture) | ||
20 | const captionfileAttach = args.mimeType ? [ captionfile, { contentType: args.mimeType } ] : captionfile | ||
21 | |||
22 | return makeUploadRequest({ | ||
23 | method: 'PUT', | ||
24 | url: args.url, | ||
25 | path, | ||
26 | token: args.accessToken, | ||
27 | fields: {}, | ||
28 | attaches: { | ||
29 | captionfile: captionfileAttach | ||
30 | }, | ||
31 | statusCodeExpected: args.statusCodeExpected || 204 | ||
32 | }) | ||
33 | } | ||
34 | |||
35 | function listVideoCaptions (url: string, videoId: string | number) { | ||
36 | const path = '/api/v1/videos/' + videoId + '/captions' | ||
37 | |||
38 | return makeGetRequest({ | ||
39 | url, | ||
40 | path, | ||
41 | statusCodeExpected: 200 | ||
42 | }) | ||
43 | } | ||
44 | |||
45 | function deleteVideoCaption (url: string, token: string, videoId: string | number, language: string) { | ||
46 | const path = '/api/v1/videos/' + videoId + '/captions/' + language | ||
47 | |||
48 | return makeDeleteRequest({ | ||
49 | url, | ||
50 | token, | ||
51 | path, | ||
52 | statusCodeExpected: 204 | ||
53 | }) | ||
54 | } | ||
55 | |||
56 | async function testCaptionFile (url: string, captionPath: string, containsString: string) { | ||
57 | const res = await request(url) | ||
58 | .get(captionPath) | ||
59 | .expect(200) | ||
60 | |||
61 | expect(res.text).to.contain(containsString) | ||
62 | } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | export { | ||
67 | createVideoCaption, | ||
68 | listVideoCaptions, | ||
69 | testCaptionFile, | ||
70 | deleteVideoCaption | ||
71 | } | ||
diff --git a/server/tests/utils/videos/video-change-ownership.ts b/server/tests/utils/videos/video-change-ownership.ts deleted file mode 100644 index f288692ea..000000000 --- a/server/tests/utils/videos/video-change-ownership.ts +++ /dev/null | |||
@@ -1,54 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | |||
3 | function changeVideoOwnership (url: string, token: string, videoId: number | string, username) { | ||
4 | const path = '/api/v1/videos/' + videoId + '/give-ownership' | ||
5 | |||
6 | return request(url) | ||
7 | .post(path) | ||
8 | .set('Accept', 'application/json') | ||
9 | .set('Authorization', 'Bearer ' + token) | ||
10 | .send({ username }) | ||
11 | .expect(204) | ||
12 | } | ||
13 | |||
14 | function getVideoChangeOwnershipList (url: string, token: string) { | ||
15 | const path = '/api/v1/videos/ownership' | ||
16 | |||
17 | return request(url) | ||
18 | .get(path) | ||
19 | .query({ sort: '-createdAt' }) | ||
20 | .set('Accept', 'application/json') | ||
21 | .set('Authorization', 'Bearer ' + token) | ||
22 | .expect(200) | ||
23 | .expect('Content-Type', /json/) | ||
24 | } | ||
25 | |||
26 | function acceptChangeOwnership (url: string, token: string, ownershipId: string, channelId: number, expectedStatus = 204) { | ||
27 | const path = '/api/v1/videos/ownership/' + ownershipId + '/accept' | ||
28 | |||
29 | return request(url) | ||
30 | .post(path) | ||
31 | .set('Accept', 'application/json') | ||
32 | .set('Authorization', 'Bearer ' + token) | ||
33 | .send({ channelId }) | ||
34 | .expect(expectedStatus) | ||
35 | } | ||
36 | |||
37 | function refuseChangeOwnership (url: string, token: string, ownershipId: string, expectedStatus = 204) { | ||
38 | const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse' | ||
39 | |||
40 | return request(url) | ||
41 | .post(path) | ||
42 | .set('Accept', 'application/json') | ||
43 | .set('Authorization', 'Bearer ' + token) | ||
44 | .expect(expectedStatus) | ||
45 | } | ||
46 | |||
47 | // --------------------------------------------------------------------------- | ||
48 | |||
49 | export { | ||
50 | changeVideoOwnership, | ||
51 | getVideoChangeOwnershipList, | ||
52 | acceptChangeOwnership, | ||
53 | refuseChangeOwnership | ||
54 | } | ||
diff --git a/server/tests/utils/videos/video-channels.ts b/server/tests/utils/videos/video-channels.ts deleted file mode 100644 index 092985777..000000000 --- a/server/tests/utils/videos/video-channels.ts +++ /dev/null | |||
@@ -1,118 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared/models/videos' | ||
3 | import { updateAvatarRequest } from '../index' | ||
4 | |||
5 | function getVideoChannelsList (url: string, start: number, count: number, sort?: string) { | ||
6 | const path = '/api/v1/video-channels' | ||
7 | |||
8 | const req = request(url) | ||
9 | .get(path) | ||
10 | .query({ start: start }) | ||
11 | .query({ count: count }) | ||
12 | |||
13 | if (sort) req.query({ sort }) | ||
14 | |||
15 | return req.set('Accept', 'application/json') | ||
16 | .expect(200) | ||
17 | .expect('Content-Type', /json/) | ||
18 | } | ||
19 | |||
20 | function getAccountVideoChannelsList (url: string, accountName: string, specialStatus = 200) { | ||
21 | const path = '/api/v1/accounts/' + accountName + '/video-channels' | ||
22 | |||
23 | return request(url) | ||
24 | .get(path) | ||
25 | .set('Accept', 'application/json') | ||
26 | .expect(specialStatus) | ||
27 | .expect('Content-Type', /json/) | ||
28 | } | ||
29 | |||
30 | function addVideoChannel ( | ||
31 | url: string, | ||
32 | token: string, | ||
33 | videoChannelAttributesArg: VideoChannelCreate, | ||
34 | expectedStatus = 200 | ||
35 | ) { | ||
36 | const path = '/api/v1/video-channels/' | ||
37 | |||
38 | // Default attributes | ||
39 | let attributes = { | ||
40 | displayName: 'my super video channel', | ||
41 | description: 'my super channel description', | ||
42 | support: 'my super channel support' | ||
43 | } | ||
44 | attributes = Object.assign(attributes, videoChannelAttributesArg) | ||
45 | |||
46 | return request(url) | ||
47 | .post(path) | ||
48 | .send(attributes) | ||
49 | .set('Accept', 'application/json') | ||
50 | .set('Authorization', 'Bearer ' + token) | ||
51 | .expect(expectedStatus) | ||
52 | } | ||
53 | |||
54 | function updateVideoChannel ( | ||
55 | url: string, | ||
56 | token: string, | ||
57 | channelName: string, | ||
58 | attributes: VideoChannelUpdate, | ||
59 | expectedStatus = 204 | ||
60 | ) { | ||
61 | const body = {} | ||
62 | const path = '/api/v1/video-channels/' + channelName | ||
63 | |||
64 | if (attributes.displayName) body['displayName'] = attributes.displayName | ||
65 | if (attributes.description) body['description'] = attributes.description | ||
66 | if (attributes.support) body['support'] = attributes.support | ||
67 | |||
68 | return request(url) | ||
69 | .put(path) | ||
70 | .send(body) | ||
71 | .set('Accept', 'application/json') | ||
72 | .set('Authorization', 'Bearer ' + token) | ||
73 | .expect(expectedStatus) | ||
74 | } | ||
75 | |||
76 | function deleteVideoChannel (url: string, token: string, channelName: string, expectedStatus = 204) { | ||
77 | const path = '/api/v1/video-channels/' + channelName | ||
78 | |||
79 | return request(url) | ||
80 | .delete(path) | ||
81 | .set('Accept', 'application/json') | ||
82 | .set('Authorization', 'Bearer ' + token) | ||
83 | .expect(expectedStatus) | ||
84 | } | ||
85 | |||
86 | function getVideoChannel (url: string, channelName: string) { | ||
87 | const path = '/api/v1/video-channels/' + channelName | ||
88 | |||
89 | return request(url) | ||
90 | .get(path) | ||
91 | .set('Accept', 'application/json') | ||
92 | .expect(200) | ||
93 | .expect('Content-Type', /json/) | ||
94 | } | ||
95 | |||
96 | function updateVideoChannelAvatar (options: { | ||
97 | url: string, | ||
98 | accessToken: string, | ||
99 | fixture: string, | ||
100 | videoChannelName: string | number | ||
101 | }) { | ||
102 | |||
103 | const path = '/api/v1/video-channels/' + options.videoChannelName + '/avatar/pick' | ||
104 | |||
105 | return updateAvatarRequest(Object.assign(options, { path })) | ||
106 | } | ||
107 | |||
108 | // --------------------------------------------------------------------------- | ||
109 | |||
110 | export { | ||
111 | updateVideoChannelAvatar, | ||
112 | getVideoChannelsList, | ||
113 | getAccountVideoChannelsList, | ||
114 | addVideoChannel, | ||
115 | updateVideoChannel, | ||
116 | deleteVideoChannel, | ||
117 | getVideoChannel | ||
118 | } | ||
diff --git a/server/tests/utils/videos/video-comments.ts b/server/tests/utils/videos/video-comments.ts deleted file mode 100644 index 1b9ee452e..000000000 --- a/server/tests/utils/videos/video-comments.ts +++ /dev/null | |||
@@ -1,83 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import { makeDeleteRequest } from '../' | ||
3 | |||
4 | function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string) { | ||
5 | const path = '/api/v1/videos/' + videoId + '/comment-threads' | ||
6 | |||
7 | const req = request(url) | ||
8 | .get(path) | ||
9 | .query({ start: start }) | ||
10 | .query({ count: count }) | ||
11 | |||
12 | if (sort) req.query({ sort }) | ||
13 | |||
14 | return req.set('Accept', 'application/json') | ||
15 | .expect(200) | ||
16 | .expect('Content-Type', /json/) | ||
17 | } | ||
18 | |||
19 | function getVideoThreadComments (url: string, videoId: number | string, threadId: number) { | ||
20 | const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId | ||
21 | |||
22 | return request(url) | ||
23 | .get(path) | ||
24 | .set('Accept', 'application/json') | ||
25 | .expect(200) | ||
26 | .expect('Content-Type', /json/) | ||
27 | } | ||
28 | |||
29 | function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) { | ||
30 | const path = '/api/v1/videos/' + videoId + '/comment-threads' | ||
31 | |||
32 | return request(url) | ||
33 | .post(path) | ||
34 | .send({ text }) | ||
35 | .set('Accept', 'application/json') | ||
36 | .set('Authorization', 'Bearer ' + token) | ||
37 | .expect(expectedStatus) | ||
38 | } | ||
39 | |||
40 | function addVideoCommentReply ( | ||
41 | url: string, | ||
42 | token: string, | ||
43 | videoId: number | string, | ||
44 | inReplyToCommentId: number, | ||
45 | text: string, | ||
46 | expectedStatus = 200 | ||
47 | ) { | ||
48 | const path = '/api/v1/videos/' + videoId + '/comments/' + inReplyToCommentId | ||
49 | |||
50 | return request(url) | ||
51 | .post(path) | ||
52 | .send({ text }) | ||
53 | .set('Accept', 'application/json') | ||
54 | .set('Authorization', 'Bearer ' + token) | ||
55 | .expect(expectedStatus) | ||
56 | } | ||
57 | |||
58 | function deleteVideoComment ( | ||
59 | url: string, | ||
60 | token: string, | ||
61 | videoId: number | string, | ||
62 | commentId: number, | ||
63 | statusCodeExpected = 204 | ||
64 | ) { | ||
65 | const path = '/api/v1/videos/' + videoId + '/comments/' + commentId | ||
66 | |||
67 | return makeDeleteRequest({ | ||
68 | url, | ||
69 | path, | ||
70 | token, | ||
71 | statusCodeExpected | ||
72 | }) | ||
73 | } | ||
74 | |||
75 | // --------------------------------------------------------------------------- | ||
76 | |||
77 | export { | ||
78 | getVideoCommentThreads, | ||
79 | getVideoThreadComments, | ||
80 | addVideoCommentThread, | ||
81 | addVideoCommentReply, | ||
82 | deleteVideoComment | ||
83 | } | ||
diff --git a/server/tests/utils/videos/video-history.ts b/server/tests/utils/videos/video-history.ts deleted file mode 100644 index 7635478f7..000000000 --- a/server/tests/utils/videos/video-history.ts +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | import { makePutBodyRequest } from '../requests/requests' | ||
2 | |||
3 | function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number) { | ||
4 | const path = '/api/v1/videos/' + videoId + '/watching' | ||
5 | const fields = { currentTime } | ||
6 | |||
7 | return makePutBodyRequest({ url, path, token, fields, statusCodeExpected: 204 }) | ||
8 | } | ||
9 | |||
10 | // --------------------------------------------------------------------------- | ||
11 | |||
12 | export { | ||
13 | userWatchVideo | ||
14 | } | ||
diff --git a/server/tests/utils/videos/video-imports.ts b/server/tests/utils/videos/video-imports.ts deleted file mode 100644 index 59dfd481a..000000000 --- a/server/tests/utils/videos/video-imports.ts +++ /dev/null | |||
@@ -1,51 +0,0 @@ | |||
1 | import { VideoImportCreate } from '../../../../shared/models/videos' | ||
2 | import { makeGetRequest, makeUploadRequest } from '..' | ||
3 | |||
4 | function getYoutubeVideoUrl () { | ||
5 | return 'https://youtu.be/msX3jv1XdvM' | ||
6 | } | ||
7 | |||
8 | function getMagnetURI () { | ||
9 | // tslint:disable:max-line-length | ||
10 | return 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4' | ||
11 | } | ||
12 | |||
13 | function importVideo (url: string, token: string, attributes: VideoImportCreate) { | ||
14 | const path = '/api/v1/videos/imports' | ||
15 | |||
16 | let attaches: any = {} | ||
17 | if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile } | ||
18 | |||
19 | return makeUploadRequest({ | ||
20 | url, | ||
21 | path, | ||
22 | token, | ||
23 | attaches, | ||
24 | fields: attributes, | ||
25 | statusCodeExpected: 200 | ||
26 | }) | ||
27 | } | ||
28 | |||
29 | function getMyVideoImports (url: string, token: string, sort?: string) { | ||
30 | const path = '/api/v1/users/me/videos/imports' | ||
31 | |||
32 | const query = {} | ||
33 | if (sort) query['sort'] = sort | ||
34 | |||
35 | return makeGetRequest({ | ||
36 | url, | ||
37 | query, | ||
38 | path, | ||
39 | token, | ||
40 | statusCodeExpected: 200 | ||
41 | }) | ||
42 | } | ||
43 | |||
44 | // --------------------------------------------------------------------------- | ||
45 | |||
46 | export { | ||
47 | getYoutubeVideoUrl, | ||
48 | importVideo, | ||
49 | getMagnetURI, | ||
50 | getMyVideoImports | ||
51 | } | ||
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts deleted file mode 100644 index bc878b039..000000000 --- a/server/tests/utils/videos/videos.ts +++ /dev/null | |||
@@ -1,582 +0,0 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { existsSync, readdir, readFile } from 'fs-extra' | ||
5 | import * as parseTorrent from 'parse-torrent' | ||
6 | import { extname, join } from 'path' | ||
7 | import * as request from 'supertest' | ||
8 | import { | ||
9 | buildAbsoluteFixturePath, | ||
10 | getMyUserInformation, | ||
11 | immutableAssign, | ||
12 | makeGetRequest, | ||
13 | makePutBodyRequest, | ||
14 | makeUploadRequest, | ||
15 | root, | ||
16 | ServerInfo, | ||
17 | testImage | ||
18 | } from '../' | ||
19 | import { VideoDetails, VideoPrivacy } from '../../../../shared/models/videos' | ||
20 | import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers' | ||
21 | import { dateIsValid, webtorrentAdd } from '../index' | ||
22 | |||
23 | type VideoAttributes = { | ||
24 | name?: string | ||
25 | category?: number | ||
26 | licence?: number | ||
27 | language?: string | ||
28 | nsfw?: boolean | ||
29 | commentsEnabled?: boolean | ||
30 | downloadEnabled?: boolean | ||
31 | waitTranscoding?: boolean | ||
32 | description?: string | ||
33 | tags?: string[] | ||
34 | channelId?: number | ||
35 | privacy?: VideoPrivacy | ||
36 | fixture?: string | ||
37 | thumbnailfile?: string | ||
38 | previewfile?: string | ||
39 | scheduleUpdate?: { | ||
40 | updateAt: string | ||
41 | privacy?: VideoPrivacy | ||
42 | } | ||
43 | } | ||
44 | |||
45 | function getVideoCategories (url: string) { | ||
46 | const path = '/api/v1/videos/categories' | ||
47 | |||
48 | return makeGetRequest({ | ||
49 | url, | ||
50 | path, | ||
51 | statusCodeExpected: 200 | ||
52 | }) | ||
53 | } | ||
54 | |||
55 | function getVideoLicences (url: string) { | ||
56 | const path = '/api/v1/videos/licences' | ||
57 | |||
58 | return makeGetRequest({ | ||
59 | url, | ||
60 | path, | ||
61 | statusCodeExpected: 200 | ||
62 | }) | ||
63 | } | ||
64 | |||
65 | function getVideoLanguages (url: string) { | ||
66 | const path = '/api/v1/videos/languages' | ||
67 | |||
68 | return makeGetRequest({ | ||
69 | url, | ||
70 | path, | ||
71 | statusCodeExpected: 200 | ||
72 | }) | ||
73 | } | ||
74 | |||
75 | function getVideoPrivacies (url: string) { | ||
76 | const path = '/api/v1/videos/privacies' | ||
77 | |||
78 | return makeGetRequest({ | ||
79 | url, | ||
80 | path, | ||
81 | statusCodeExpected: 200 | ||
82 | }) | ||
83 | } | ||
84 | |||
85 | function getVideo (url: string, id: number | string, expectedStatus = 200) { | ||
86 | const path = '/api/v1/videos/' + id | ||
87 | |||
88 | return request(url) | ||
89 | .get(path) | ||
90 | .set('Accept', 'application/json') | ||
91 | .expect(expectedStatus) | ||
92 | } | ||
93 | |||
94 | function viewVideo (url: string, id: number | string, expectedStatus = 204, xForwardedFor?: string) { | ||
95 | const path = '/api/v1/videos/' + id + '/views' | ||
96 | |||
97 | const req = request(url) | ||
98 | .post(path) | ||
99 | .set('Accept', 'application/json') | ||
100 | |||
101 | if (xForwardedFor) { | ||
102 | req.set('X-Forwarded-For', xForwardedFor) | ||
103 | } | ||
104 | |||
105 | return req.expect(expectedStatus) | ||
106 | } | ||
107 | |||
108 | function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) { | ||
109 | const path = '/api/v1/videos/' + id | ||
110 | |||
111 | return request(url) | ||
112 | .get(path) | ||
113 | .set('Authorization', 'Bearer ' + token) | ||
114 | .set('Accept', 'application/json') | ||
115 | .expect(expectedStatus) | ||
116 | } | ||
117 | |||
118 | function getVideoDescription (url: string, descriptionPath: string) { | ||
119 | return request(url) | ||
120 | .get(descriptionPath) | ||
121 | .set('Accept', 'application/json') | ||
122 | .expect(200) | ||
123 | .expect('Content-Type', /json/) | ||
124 | } | ||
125 | |||
126 | function getVideosList (url: string) { | ||
127 | const path = '/api/v1/videos' | ||
128 | |||
129 | return request(url) | ||
130 | .get(path) | ||
131 | .query({ sort: 'name' }) | ||
132 | .set('Accept', 'application/json') | ||
133 | .expect(200) | ||
134 | .expect('Content-Type', /json/) | ||
135 | } | ||
136 | |||
137 | function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) { | ||
138 | const path = '/api/v1/videos' | ||
139 | |||
140 | return request(url) | ||
141 | .get(path) | ||
142 | .set('Authorization', 'Bearer ' + token) | ||
143 | .query(immutableAssign(query, { sort: 'name' })) | ||
144 | .set('Accept', 'application/json') | ||
145 | .expect(200) | ||
146 | .expect('Content-Type', /json/) | ||
147 | } | ||
148 | |||
149 | function getLocalVideos (url: string) { | ||
150 | const path = '/api/v1/videos' | ||
151 | |||
152 | return request(url) | ||
153 | .get(path) | ||
154 | .query({ sort: 'name', filter: 'local' }) | ||
155 | .set('Accept', 'application/json') | ||
156 | .expect(200) | ||
157 | .expect('Content-Type', /json/) | ||
158 | } | ||
159 | |||
160 | function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string) { | ||
161 | const path = '/api/v1/users/me/videos' | ||
162 | |||
163 | const req = request(url) | ||
164 | .get(path) | ||
165 | .query({ start: start }) | ||
166 | .query({ count: count }) | ||
167 | |||
168 | if (sort) req.query({ sort }) | ||
169 | |||
170 | return req.set('Accept', 'application/json') | ||
171 | .set('Authorization', 'Bearer ' + accessToken) | ||
172 | .expect(200) | ||
173 | .expect('Content-Type', /json/) | ||
174 | } | ||
175 | |||
176 | function getAccountVideos ( | ||
177 | url: string, | ||
178 | accessToken: string, | ||
179 | accountName: string, | ||
180 | start: number, | ||
181 | count: number, | ||
182 | sort?: string, | ||
183 | query: { nsfw?: boolean } = {} | ||
184 | ) { | ||
185 | const path = '/api/v1/accounts/' + accountName + '/videos' | ||
186 | |||
187 | return makeGetRequest({ | ||
188 | url, | ||
189 | path, | ||
190 | query: immutableAssign(query, { | ||
191 | start, | ||
192 | count, | ||
193 | sort | ||
194 | }), | ||
195 | token: accessToken, | ||
196 | statusCodeExpected: 200 | ||
197 | }) | ||
198 | } | ||
199 | |||
200 | function getVideoChannelVideos ( | ||
201 | url: string, | ||
202 | accessToken: string, | ||
203 | videoChannelName: string, | ||
204 | start: number, | ||
205 | count: number, | ||
206 | sort?: string, | ||
207 | query: { nsfw?: boolean } = {} | ||
208 | ) { | ||
209 | const path = '/api/v1/video-channels/' + videoChannelName + '/videos' | ||
210 | |||
211 | return makeGetRequest({ | ||
212 | url, | ||
213 | path, | ||
214 | query: immutableAssign(query, { | ||
215 | start, | ||
216 | count, | ||
217 | sort | ||
218 | }), | ||
219 | token: accessToken, | ||
220 | statusCodeExpected: 200 | ||
221 | }) | ||
222 | } | ||
223 | |||
224 | function getVideosListPagination (url: string, start: number, count: number, sort?: string) { | ||
225 | const path = '/api/v1/videos' | ||
226 | |||
227 | const req = request(url) | ||
228 | .get(path) | ||
229 | .query({ start: start }) | ||
230 | .query({ count: count }) | ||
231 | |||
232 | if (sort) req.query({ sort }) | ||
233 | |||
234 | return req.set('Accept', 'application/json') | ||
235 | .expect(200) | ||
236 | .expect('Content-Type', /json/) | ||
237 | } | ||
238 | |||
239 | function getVideosListSort (url: string, sort: string) { | ||
240 | const path = '/api/v1/videos' | ||
241 | |||
242 | return request(url) | ||
243 | .get(path) | ||
244 | .query({ sort: sort }) | ||
245 | .set('Accept', 'application/json') | ||
246 | .expect(200) | ||
247 | .expect('Content-Type', /json/) | ||
248 | } | ||
249 | |||
250 | function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) { | ||
251 | const path = '/api/v1/videos' | ||
252 | |||
253 | return request(url) | ||
254 | .get(path) | ||
255 | .query(query) | ||
256 | .set('Accept', 'application/json') | ||
257 | .expect(200) | ||
258 | .expect('Content-Type', /json/) | ||
259 | } | ||
260 | |||
261 | function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) { | ||
262 | const path = '/api/v1/videos' | ||
263 | |||
264 | return request(url) | ||
265 | .delete(path + '/' + id) | ||
266 | .set('Accept', 'application/json') | ||
267 | .set('Authorization', 'Bearer ' + token) | ||
268 | .expect(expectedStatus) | ||
269 | } | ||
270 | |||
271 | async function checkVideoFilesWereRemoved ( | ||
272 | videoUUID: string, | ||
273 | serverNumber: number, | ||
274 | directories = [ 'videos', 'thumbnails', 'torrents', 'previews', 'captions' ] | ||
275 | ) { | ||
276 | const testDirectory = 'test' + serverNumber | ||
277 | |||
278 | for (const directory of directories) { | ||
279 | const directoryPath = join(root(), testDirectory, directory) | ||
280 | |||
281 | const directoryExists = existsSync(directoryPath) | ||
282 | expect(directoryExists).to.be.true | ||
283 | |||
284 | const files = await readdir(directoryPath) | ||
285 | for (const file of files) { | ||
286 | expect(file).to.not.contain(videoUUID) | ||
287 | } | ||
288 | } | ||
289 | } | ||
290 | |||
291 | async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) { | ||
292 | const path = '/api/v1/videos/upload' | ||
293 | let defaultChannelId = '1' | ||
294 | |||
295 | try { | ||
296 | const res = await getMyUserInformation(url, accessToken) | ||
297 | defaultChannelId = res.body.videoChannels[0].id | ||
298 | } catch (e) { /* empty */ } | ||
299 | |||
300 | // Override default attributes | ||
301 | const attributes = Object.assign({ | ||
302 | name: 'my super video', | ||
303 | category: 5, | ||
304 | licence: 4, | ||
305 | language: 'zh', | ||
306 | channelId: defaultChannelId, | ||
307 | nsfw: true, | ||
308 | waitTranscoding: false, | ||
309 | description: 'my super description', | ||
310 | support: 'my super support text', | ||
311 | tags: [ 'tag' ], | ||
312 | privacy: VideoPrivacy.PUBLIC, | ||
313 | commentsEnabled: true, | ||
314 | downloadEnabled: true, | ||
315 | fixture: 'video_short.webm' | ||
316 | }, videoAttributesArg) | ||
317 | |||
318 | const req = request(url) | ||
319 | .post(path) | ||
320 | .set('Accept', 'application/json') | ||
321 | .set('Authorization', 'Bearer ' + accessToken) | ||
322 | .field('name', attributes.name) | ||
323 | .field('nsfw', JSON.stringify(attributes.nsfw)) | ||
324 | .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled)) | ||
325 | .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled)) | ||
326 | .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding)) | ||
327 | .field('privacy', attributes.privacy.toString()) | ||
328 | .field('channelId', attributes.channelId) | ||
329 | |||
330 | if (attributes.description !== undefined) { | ||
331 | req.field('description', attributes.description) | ||
332 | } | ||
333 | if (attributes.language !== undefined) { | ||
334 | req.field('language', attributes.language.toString()) | ||
335 | } | ||
336 | if (attributes.category !== undefined) { | ||
337 | req.field('category', attributes.category.toString()) | ||
338 | } | ||
339 | if (attributes.licence !== undefined) { | ||
340 | req.field('licence', attributes.licence.toString()) | ||
341 | } | ||
342 | |||
343 | for (let i = 0; i < attributes.tags.length; i++) { | ||
344 | req.field('tags[' + i + ']', attributes.tags[i]) | ||
345 | } | ||
346 | |||
347 | if (attributes.thumbnailfile !== undefined) { | ||
348 | req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile)) | ||
349 | } | ||
350 | if (attributes.previewfile !== undefined) { | ||
351 | req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile)) | ||
352 | } | ||
353 | |||
354 | if (attributes.scheduleUpdate) { | ||
355 | req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt) | ||
356 | |||
357 | if (attributes.scheduleUpdate.privacy) { | ||
358 | req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy) | ||
359 | } | ||
360 | } | ||
361 | |||
362 | return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture)) | ||
363 | .expect(specialStatus) | ||
364 | } | ||
365 | |||
366 | function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) { | ||
367 | const path = '/api/v1/videos/' + id | ||
368 | const body = {} | ||
369 | |||
370 | if (attributes.name) body['name'] = attributes.name | ||
371 | if (attributes.category) body['category'] = attributes.category | ||
372 | if (attributes.licence) body['licence'] = attributes.licence | ||
373 | if (attributes.language) body['language'] = attributes.language | ||
374 | if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw) | ||
375 | if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled) | ||
376 | if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled) | ||
377 | if (attributes.description) body['description'] = attributes.description | ||
378 | if (attributes.tags) body['tags'] = attributes.tags | ||
379 | if (attributes.privacy) body['privacy'] = attributes.privacy | ||
380 | if (attributes.channelId) body['channelId'] = attributes.channelId | ||
381 | if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate | ||
382 | |||
383 | // Upload request | ||
384 | if (attributes.thumbnailfile || attributes.previewfile) { | ||
385 | const attaches: any = {} | ||
386 | if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile | ||
387 | if (attributes.previewfile) attaches.previewfile = attributes.previewfile | ||
388 | |||
389 | return makeUploadRequest({ | ||
390 | url, | ||
391 | method: 'PUT', | ||
392 | path, | ||
393 | token: accessToken, | ||
394 | fields: body, | ||
395 | attaches, | ||
396 | statusCodeExpected | ||
397 | }) | ||
398 | } | ||
399 | |||
400 | return makePutBodyRequest({ | ||
401 | url, | ||
402 | path, | ||
403 | fields: body, | ||
404 | token: accessToken, | ||
405 | statusCodeExpected | ||
406 | }) | ||
407 | } | ||
408 | |||
409 | function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) { | ||
410 | const path = '/api/v1/videos/' + id + '/rate' | ||
411 | |||
412 | return request(url) | ||
413 | .put(path) | ||
414 | .set('Accept', 'application/json') | ||
415 | .set('Authorization', 'Bearer ' + accessToken) | ||
416 | .send({ rating }) | ||
417 | .expect(specialStatus) | ||
418 | } | ||
419 | |||
420 | function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) { | ||
421 | return new Promise<any>((res, rej) => { | ||
422 | const torrentName = videoUUID + '-' + resolution + '.torrent' | ||
423 | const torrentPath = join(__dirname, '..', '..', '..', '..', 'test' + server.serverNumber, 'torrents', torrentName) | ||
424 | readFile(torrentPath, (err, data) => { | ||
425 | if (err) return rej(err) | ||
426 | |||
427 | return res(parseTorrent(data)) | ||
428 | }) | ||
429 | }) | ||
430 | } | ||
431 | |||
432 | async function completeVideoCheck ( | ||
433 | url: string, | ||
434 | video: any, | ||
435 | attributes: { | ||
436 | name: string | ||
437 | category: number | ||
438 | licence: number | ||
439 | language: string | ||
440 | nsfw: boolean | ||
441 | commentsEnabled: boolean | ||
442 | downloadEnabled: boolean | ||
443 | description: string | ||
444 | publishedAt?: string | ||
445 | support: string | ||
446 | account: { | ||
447 | name: string | ||
448 | host: string | ||
449 | } | ||
450 | isLocal: boolean | ||
451 | tags: string[] | ||
452 | privacy: number | ||
453 | likes?: number | ||
454 | dislikes?: number | ||
455 | duration: number | ||
456 | channel: { | ||
457 | displayName: string | ||
458 | name: string | ||
459 | description | ||
460 | isLocal: boolean | ||
461 | } | ||
462 | fixture: string | ||
463 | files: { | ||
464 | resolution: number | ||
465 | size: number | ||
466 | }[], | ||
467 | thumbnailfile?: string | ||
468 | previewfile?: string | ||
469 | } | ||
470 | ) { | ||
471 | if (!attributes.likes) attributes.likes = 0 | ||
472 | if (!attributes.dislikes) attributes.dislikes = 0 | ||
473 | |||
474 | expect(video.name).to.equal(attributes.name) | ||
475 | expect(video.category.id).to.equal(attributes.category) | ||
476 | expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc') | ||
477 | expect(video.licence.id).to.equal(attributes.licence) | ||
478 | expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown') | ||
479 | expect(video.language.id).to.equal(attributes.language) | ||
480 | expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown') | ||
481 | expect(video.privacy.id).to.deep.equal(attributes.privacy) | ||
482 | expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy]) | ||
483 | expect(video.nsfw).to.equal(attributes.nsfw) | ||
484 | expect(video.description).to.equal(attributes.description) | ||
485 | expect(video.account.id).to.be.a('number') | ||
486 | expect(video.account.uuid).to.be.a('string') | ||
487 | expect(video.account.host).to.equal(attributes.account.host) | ||
488 | expect(video.account.name).to.equal(attributes.account.name) | ||
489 | expect(video.channel.displayName).to.equal(attributes.channel.displayName) | ||
490 | expect(video.channel.name).to.equal(attributes.channel.name) | ||
491 | expect(video.likes).to.equal(attributes.likes) | ||
492 | expect(video.dislikes).to.equal(attributes.dislikes) | ||
493 | expect(video.isLocal).to.equal(attributes.isLocal) | ||
494 | expect(video.duration).to.equal(attributes.duration) | ||
495 | expect(dateIsValid(video.createdAt)).to.be.true | ||
496 | expect(dateIsValid(video.publishedAt)).to.be.true | ||
497 | expect(dateIsValid(video.updatedAt)).to.be.true | ||
498 | |||
499 | if (attributes.publishedAt) { | ||
500 | expect(video.publishedAt).to.equal(attributes.publishedAt) | ||
501 | } | ||
502 | |||
503 | const res = await getVideo(url, video.uuid) | ||
504 | const videoDetails: VideoDetails = res.body | ||
505 | |||
506 | expect(videoDetails.files).to.have.lengthOf(attributes.files.length) | ||
507 | expect(videoDetails.tags).to.deep.equal(attributes.tags) | ||
508 | expect(videoDetails.account.name).to.equal(attributes.account.name) | ||
509 | expect(videoDetails.account.host).to.equal(attributes.account.host) | ||
510 | expect(video.channel.displayName).to.equal(attributes.channel.displayName) | ||
511 | expect(video.channel.name).to.equal(attributes.channel.name) | ||
512 | expect(videoDetails.channel.host).to.equal(attributes.account.host) | ||
513 | expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal) | ||
514 | expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true | ||
515 | expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true | ||
516 | expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled) | ||
517 | expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled) | ||
518 | |||
519 | for (const attributeFile of attributes.files) { | ||
520 | const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution) | ||
521 | expect(file).not.to.be.undefined | ||
522 | |||
523 | let extension = extname(attributes.fixture) | ||
524 | // Transcoding enabled on server 2, extension will always be .mp4 | ||
525 | if (attributes.account.host === 'localhost:9002') extension = '.mp4' | ||
526 | |||
527 | const magnetUri = file.magnetUri | ||
528 | expect(file.magnetUri).to.have.lengthOf.above(2) | ||
529 | expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`) | ||
530 | expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`) | ||
531 | expect(file.resolution.id).to.equal(attributeFile.resolution) | ||
532 | expect(file.resolution.label).to.equal(attributeFile.resolution + 'p') | ||
533 | |||
534 | const minSize = attributeFile.size - ((10 * attributeFile.size) / 100) | ||
535 | const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100) | ||
536 | expect(file.size, | ||
537 | 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')') | ||
538 | .to.be.above(minSize).and.below(maxSize) | ||
539 | |||
540 | { | ||
541 | await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) | ||
542 | } | ||
543 | |||
544 | if (attributes.previewfile) { | ||
545 | await testImage(url, attributes.previewfile, videoDetails.previewPath) | ||
546 | } | ||
547 | |||
548 | const torrent = await webtorrentAdd(magnetUri, true) | ||
549 | expect(torrent.files).to.be.an('array') | ||
550 | expect(torrent.files.length).to.equal(1) | ||
551 | expect(torrent.files[0].path).to.exist.and.to.not.equal('') | ||
552 | } | ||
553 | } | ||
554 | |||
555 | // --------------------------------------------------------------------------- | ||
556 | |||
557 | export { | ||
558 | getVideoDescription, | ||
559 | getVideoCategories, | ||
560 | getVideoLicences, | ||
561 | getVideoPrivacies, | ||
562 | getVideoLanguages, | ||
563 | getMyVideos, | ||
564 | getAccountVideos, | ||
565 | getVideoChannelVideos, | ||
566 | getVideo, | ||
567 | getVideoWithToken, | ||
568 | getVideosList, | ||
569 | getVideosListPagination, | ||
570 | getVideosListSort, | ||
571 | removeVideo, | ||
572 | getVideosListWithToken, | ||
573 | uploadVideo, | ||
574 | getVideosWithFilters, | ||
575 | updateVideo, | ||
576 | rateVideo, | ||
577 | viewVideo, | ||
578 | parseTorrentVideo, | ||
579 | getLocalVideos, | ||
580 | completeVideoCheck, | ||
581 | checkVideoFilesWereRemoved | ||
582 | } | ||
diff --git a/server/tools/README.md b/server/tools/README.md new file mode 100644 index 000000000..6b94d74e5 --- /dev/null +++ b/server/tools/README.md | |||
@@ -0,0 +1,82 @@ | |||
1 | peertube(8) -- companion CLI for PeerTube | ||
2 | ========================================= | ||
3 | |||
4 | SYNOPSIS | ||
5 | -------- | ||
6 | |||
7 | ``` | ||
8 | peertube [command] [options] | ||
9 | ``` | ||
10 | |||
11 | DESCRIPTION | ||
12 | ----------- | ||
13 | |||
14 | `peertube` wraps various utilities around PeerTube that are used either on a running local, running remote, or cold local instance. | ||
15 | |||
16 | COMMANDS | ||
17 | -------- | ||
18 | |||
19 | Unless otherwise specified, every command can be queried for its own help or manual by passing its name to the `help` command, or by using the `--help` option. | ||
20 | |||
21 | `auth [action]`: stores credentials for your accounts on remote instances so that you don't need to pass them at every command | ||
22 | |||
23 | `upload|up`: upload a video to a remote instance | ||
24 | |||
25 | $ peertube upload \ | ||
26 | -u "PEERTUBE_URL" \ | ||
27 | -U "PEERTUBE_USER" \ | ||
28 | --password "PEERTUBE_PASSWORD" | ||
29 | |||
30 | `import-videos|import`: import a video from a streaming platform to a remote instance | ||
31 | |||
32 | $ peertube import \ | ||
33 | -u "PEERTUBE_URL" \ | ||
34 | -U "PEERTUBE_USER" \ | ||
35 | --password "PEERTUBE_PASSWORD" \ | ||
36 | -t "TARGET_URL" | ||
37 | |||
38 | The target URL can be directly the video file, or any of the supported sites of youtube-dl. The video is downloaded locally and then uploaded. Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection… | ||
39 | |||
40 | `watch|w`: watch a video in the terminal ✩°。⋆ | ||
41 | |||
42 | -g, --gui <player> player type (default: ascii) | ||
43 | -i, --invert invert colors (ascii player only) | ||
44 | -r, --resolution <res> video resolution (default: 720) | ||
45 | |||
46 | It provides support for different players: | ||
47 | |||
48 | - ascii (default ; plays in ascii art in your terminal!) | ||
49 | - mpv | ||
50 | - mplayer | ||
51 | - vlc | ||
52 | - stdout | ||
53 | - xbmc | ||
54 | - airplay | ||
55 | - chromecast | ||
56 | |||
57 | `repl`: interact with the application libraries and objects even when PeerTube is not running | ||
58 | |||
59 | Type .help to see the repl-only functions, or to see the available PeerTube core functions: | ||
60 | |||
61 | repl> lodash.keys(context) | ||
62 | |||
63 | `help [cmd]`: display help for [cmd] | ||
64 | |||
65 | EXAMPLES | ||
66 | -------- | ||
67 | |||
68 | $ peertube auth add -u "PEERTUBE_URL" -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD" | ||
69 | $ peertube up <videoFile> | ||
70 | $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10 | ||
71 | |||
72 | SEE ALSO | ||
73 | -------- | ||
74 | |||
75 | [PeerTube Tools Documentation](https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/tools.md) | ||
76 | |||
77 | [PeerTube Admin Documentation](https://docs.joinpeertube.org/lang/en/docs/) | ||
78 | |||
79 | REPORTING BUGS | ||
80 | -------------- | ||
81 | |||
82 | See [PeerTube repository](https://github.com/Chocobozzz/PeerTube). | ||
diff --git a/server/tools/cli.ts b/server/tools/cli.ts index 53b20964e..108c44218 100644 --- a/server/tools/cli.ts +++ b/server/tools/cli.ts | |||
@@ -23,7 +23,7 @@ async function getSettings () { | |||
23 | if (err) { | 23 | if (err) { |
24 | return rej(err) | 24 | return rej(err) |
25 | } | 25 | } |
26 | return res(data || settings) | 26 | return res(Object.keys(data).length === 0 ? settings : data) |
27 | }) | 27 | }) |
28 | }) | 28 | }) |
29 | } | 29 | } |
diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts index 33438811e..a962944a4 100644 --- a/server/tools/peertube-auth.ts +++ b/server/tools/peertube-auth.ts | |||
@@ -5,34 +5,25 @@ import { getSettings, writeSettings, netrc } from './cli' | |||
5 | import { isHostValid } from '../helpers/custom-validators/servers' | 5 | import { isHostValid } from '../helpers/custom-validators/servers' |
6 | import { isUserUsernameValid } from '../helpers/custom-validators/users' | 6 | import { isUserUsernameValid } from '../helpers/custom-validators/users' |
7 | 7 | ||
8 | function delInstance (url: string) { | 8 | async function delInstance (url: string) { |
9 | return new Promise((res, rej): void => { | 9 | const settings = await getSettings() |
10 | getSettings() | 10 | |
11 | .then(async (settings) => { | 11 | settings.remotes.splice(settings.remotes.indexOf(url)) |
12 | settings.remotes.splice(settings.remotes.indexOf(url)) | 12 | await writeSettings(settings) |
13 | await writeSettings(settings) | 13 | |
14 | delete netrc.machines[url] | 14 | delete netrc.machines[url] |
15 | netrc.save() | 15 | await netrc.save() |
16 | res() | ||
17 | }) | ||
18 | .catch(err => rej(err)) | ||
19 | }) | ||
20 | } | 16 | } |
21 | 17 | ||
22 | async function setInstance (url: string, username: string, password: string) { | 18 | async function setInstance (url: string, username: string, password: string) { |
23 | return new Promise((res, rej): void => { | 19 | const settings = await getSettings() |
24 | getSettings() | 20 | if (settings.remotes.indexOf(url) === -1) { |
25 | .then(async settings => { | 21 | settings.remotes.push(url) |
26 | if (settings.remotes.indexOf(url) === -1) { | 22 | } |
27 | settings.remotes.push(url) | 23 | await writeSettings(settings) |
28 | } | 24 | |
29 | await writeSettings(settings) | 25 | netrc.machines[url] = { login: username, password } |
30 | netrc.machines[url] = { login: username, password } | 26 | await netrc.save() |
31 | netrc.save() | ||
32 | res() | ||
33 | }) | ||
34 | .catch(err => rej(err)) | ||
35 | }) | ||
36 | } | 27 | } |
37 | 28 | ||
38 | function isURLaPeerTubeInstance (url: string) { | 29 | function isURLaPeerTubeInstance (url: string) { |
@@ -71,56 +62,60 @@ program | |||
71 | required: true | 62 | required: true |
72 | } | 63 | } |
73 | } | 64 | } |
74 | }, (_, result) => { | 65 | }, async (_, result) => { |
75 | setInstance(result.url, result.username, result.password) | 66 | await setInstance(result.url, result.username, result.password) |
67 | |||
68 | process.exit(0) | ||
76 | }) | 69 | }) |
77 | }) | 70 | }) |
78 | 71 | ||
79 | program | 72 | program |
80 | .command('del <url>') | 73 | .command('del <url>') |
81 | .description('unregisters a remote instance') | 74 | .description('unregisters a remote instance') |
82 | .action((url) => { | 75 | .action(async url => { |
83 | delInstance(url) | 76 | await delInstance(url) |
77 | |||
78 | process.exit(0) | ||
84 | }) | 79 | }) |
85 | 80 | ||
86 | program | 81 | program |
87 | .command('list') | 82 | .command('list') |
88 | .description('lists registered remote instances') | 83 | .description('lists registered remote instances') |
89 | .action(() => { | 84 | .action(async () => { |
90 | getSettings() | 85 | const settings = await getSettings() |
91 | .then(settings => { | 86 | const table = new Table({ |
92 | const table = new Table({ | 87 | head: ['instance', 'login'], |
93 | head: ['instance', 'login'], | 88 | colWidths: [30, 30] |
94 | colWidths: [30, 30] | 89 | }) |
95 | }) | 90 | netrc.loadSync() |
96 | netrc.loadSync() | 91 | settings.remotes.forEach(element => { |
97 | settings.remotes.forEach(element => { | 92 | table.push([ |
98 | table.push([ | 93 | element, |
99 | element, | 94 | netrc.machines[element].login |
100 | netrc.machines[element].login | 95 | ]) |
101 | ]) | 96 | }) |
102 | }) | 97 | |
103 | 98 | console.log(table.toString()) | |
104 | console.log(table.toString()) | 99 | |
105 | }) | 100 | process.exit(0) |
106 | }) | 101 | }) |
107 | 102 | ||
108 | program | 103 | program |
109 | .command('set-default <url>') | 104 | .command('set-default <url>') |
110 | .description('set an existing entry as default') | 105 | .description('set an existing entry as default') |
111 | .action((url) => { | 106 | .action(async url => { |
112 | getSettings() | 107 | const settings = await getSettings() |
113 | .then(settings => { | 108 | const instanceExists = settings.remotes.indexOf(url) !== -1 |
114 | const instanceExists = settings.remotes.indexOf(url) !== -1 | 109 | |
115 | 110 | if (instanceExists) { | |
116 | if (instanceExists) { | 111 | settings.default = settings.remotes.indexOf(url) |
117 | settings.default = settings.remotes.indexOf(url) | 112 | await writeSettings(settings) |
118 | writeSettings(settings) | 113 | |
119 | } else { | 114 | process.exit(0) |
120 | console.log('<url> is not a registered instance.') | 115 | } else { |
121 | process.exit(-1) | 116 | console.log('<url> is not a registered instance.') |
122 | } | 117 | process.exit(-1) |
123 | }) | 118 | } |
124 | }) | 119 | }) |
125 | 120 | ||
126 | program.on('--help', function () { | 121 | program.on('--help', function () { |
diff --git a/server/tools/peertube-get-access-token.ts b/server/tools/peertube-get-access-token.ts index eb2571a03..a68665f5b 100644 --- a/server/tools/peertube-get-access-token.ts +++ b/server/tools/peertube-get-access-token.ts | |||
@@ -6,7 +6,7 @@ import { | |||
6 | Server, | 6 | Server, |
7 | Client, | 7 | Client, |
8 | User | 8 | User |
9 | } from '../tests/utils/index' | 9 | } from '../../shared/utils' |
10 | 10 | ||
11 | program | 11 | program |
12 | .option('-u, --url <url>', 'Server url') | 12 | .option('-u, --url <url>', 'Server url') |
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts index 15f517cab..151c5a989 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts | |||
@@ -6,10 +6,11 @@ import { join } from 'path' | |||
6 | import { VideoPrivacy } from '../../shared/models/videos' | 6 | import { VideoPrivacy } from '../../shared/models/videos' |
7 | import { doRequestAndSaveToFile } from '../helpers/requests' | 7 | import { doRequestAndSaveToFile } from '../helpers/requests' |
8 | import { CONSTRAINTS_FIELDS } from '../initializers' | 8 | import { CONSTRAINTS_FIELDS } from '../initializers' |
9 | import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../tests/utils' | 9 | import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/utils/index' |
10 | import { truncate } from 'lodash' | 10 | import { truncate } from 'lodash' |
11 | import * as prompt from 'prompt' | 11 | import * as prompt from 'prompt' |
12 | import { remove } from 'fs-extra' | 12 | import { remove } from 'fs-extra' |
13 | import { sha256 } from '../helpers/core-utils' | ||
13 | import { safeGetYoutubeDL } from '../helpers/youtube-dl' | 14 | import { safeGetYoutubeDL } from '../helpers/youtube-dl' |
14 | import { getSettings, netrc } from './cli' | 15 | import { getSettings, netrc } from './cli' |
15 | 16 | ||
@@ -57,6 +58,7 @@ getSettings() | |||
57 | settings.remotes[settings.default] : | 58 | settings.remotes[settings.default] : |
58 | settings.remotes[0] | 59 | settings.remotes[0] |
59 | } | 60 | } |
61 | |||
60 | if (!program['username']) program['username'] = netrc.machines[program['url']].login | 62 | if (!program['username']) program['username'] = netrc.machines[program['url']].login |
61 | if (!program['password']) program['password'] = netrc.machines[program['url']].password | 63 | if (!program['password']) program['password'] = netrc.machines[program['url']].password |
62 | } | 64 | } |
@@ -68,12 +70,19 @@ getSettings() | |||
68 | process.exit(-1) | 70 | process.exit(-1) |
69 | } | 71 | } |
70 | 72 | ||
73 | removeEndSlashes(program['url']) | ||
74 | removeEndSlashes(program['targetUrl']) | ||
75 | |||
71 | const user = { | 76 | const user = { |
72 | username: program['username'], | 77 | username: program['username'], |
73 | password: program['password'] | 78 | password: program['password'] |
74 | } | 79 | } |
75 | 80 | ||
76 | run(user, program['url']).catch(err => console.error(err)) | 81 | run(user, program['url']) |
82 | .catch(err => { | ||
83 | console.error(err) | ||
84 | process.exit(-1) | ||
85 | }) | ||
77 | }) | 86 | }) |
78 | 87 | ||
79 | async function promptPassword () { | 88 | async function promptPassword () { |
@@ -107,8 +116,12 @@ async function run (user, url: string) { | |||
107 | secret: res.body.client_secret | 116 | secret: res.body.client_secret |
108 | } | 117 | } |
109 | 118 | ||
110 | const res2 = await login(url, client, user) | 119 | try { |
111 | accessToken = res2.body.access_token | 120 | const res = await login(program[ 'url' ], client, user) |
121 | accessToken = res.body.access_token | ||
122 | } catch (err) { | ||
123 | throw new Error('Cannot authenticate. Please check your username/password.') | ||
124 | } | ||
112 | 125 | ||
113 | const youtubeDL = await safeGetYoutubeDL() | 126 | const youtubeDL = await safeGetYoutubeDL() |
114 | 127 | ||
@@ -133,8 +146,7 @@ async function run (user, url: string) { | |||
133 | await processVideo(info, program['language'], processOptions.cwd, url, user) | 146 | await processVideo(info, program['language'], processOptions.cwd, url, user) |
134 | } | 147 | } |
135 | 148 | ||
136 | // https://www.youtube.com/watch?v=2Upx39TBc1s | 149 | console.log('Video/s for user %s imported: %s', program['username'], program['targetUrl']) |
137 | console.log('I\'m finished!') | ||
138 | process.exit(0) | 150 | process.exit(0) |
139 | }) | 151 | }) |
140 | } | 152 | } |
@@ -155,7 +167,7 @@ function processVideo (info: any, languageCode: string, cwd: string, url: string | |||
155 | return res() | 167 | return res() |
156 | } | 168 | } |
157 | 169 | ||
158 | const path = join(cwd, new Date().getTime() + '.mp4') | 170 | const path = join(cwd, sha256(videoInfo.url) + '.mp4') |
159 | 171 | ||
160 | console.log('Downloading video "%s"...', videoInfo.title) | 172 | console.log('Downloading video "%s"...', videoInfo.title) |
161 | 173 | ||
@@ -192,7 +204,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st | |||
192 | 204 | ||
193 | let thumbnailfile | 205 | let thumbnailfile |
194 | if (videoInfo.thumbnail) { | 206 | if (videoInfo.thumbnail) { |
195 | thumbnailfile = join(cwd, 'thumbnail.jpg') | 207 | thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg') |
196 | 208 | ||
197 | await doRequestAndSaveToFile({ | 209 | await doRequestAndSaveToFile({ |
198 | method: 'GET', | 210 | method: 'GET', |
@@ -322,3 +334,9 @@ function isNSFW (info: any) { | |||
322 | 334 | ||
323 | return false | 335 | return false |
324 | } | 336 | } |
337 | |||
338 | function removeEndSlashes (url: string) { | ||
339 | while (url.endsWith('/')) { | ||
340 | url.slice(0, -1) | ||
341 | } | ||
342 | } | ||
diff --git a/server/tools/peertube-repl.ts b/server/tools/peertube-repl.ts new file mode 100644 index 000000000..04d8b95a3 --- /dev/null +++ b/server/tools/peertube-repl.ts | |||
@@ -0,0 +1,76 @@ | |||
1 | import * as repl from 'repl' | ||
2 | import * as path from 'path' | ||
3 | import * as _ from 'lodash' | ||
4 | import * as uuidv1 from 'uuid/v1' | ||
5 | import * as uuidv3 from 'uuid/v3' | ||
6 | import * as uuidv4 from 'uuid/v4' | ||
7 | import * as uuidv5 from 'uuid/v5' | ||
8 | import * as Sequelize from 'sequelize' | ||
9 | import * as YoutubeDL from 'youtube-dl' | ||
10 | |||
11 | import { initDatabaseModels, sequelizeTypescript } from '../initializers' | ||
12 | import * as cli from '../tools/cli' | ||
13 | import { logger } from '../helpers/logger' | ||
14 | import * as constants from '../initializers/constants' | ||
15 | import * as modelsUtils from '../models/utils' | ||
16 | import * as coreUtils from '../helpers/core-utils' | ||
17 | import * as ffmpegUtils from '../helpers/ffmpeg-utils' | ||
18 | import * as peertubeCryptoUtils from '../helpers/peertube-crypto' | ||
19 | import * as signupUtils from '../helpers/signup' | ||
20 | import * as utils from '../helpers/utils' | ||
21 | import * as YoutubeDLUtils from '../helpers/youtube-dl' | ||
22 | |||
23 | const start = async () => { | ||
24 | await initDatabaseModels(true) | ||
25 | |||
26 | const versionCommitHash = await utils.getServerCommit() | ||
27 | |||
28 | const initContext = (replServer) => { | ||
29 | return (context) => { | ||
30 | const properties = { | ||
31 | context, repl: replServer, env: process.env, | ||
32 | lodash: _, path, | ||
33 | uuidv1, uuidv3, uuidv4, uuidv5, | ||
34 | cli, logger, constants, | ||
35 | Sequelize, sequelizeTypescript, modelsUtils, | ||
36 | models: sequelizeTypescript.models, transaction: sequelizeTypescript.transaction, | ||
37 | query: sequelizeTypescript.query, queryInterface: sequelizeTypescript.getQueryInterface(), | ||
38 | YoutubeDL, | ||
39 | coreUtils, ffmpegUtils, peertubeCryptoUtils, signupUtils, utils, YoutubeDLUtils | ||
40 | } | ||
41 | |||
42 | for (let prop in properties) { | ||
43 | Object.defineProperty(context, prop, { | ||
44 | configurable: false, | ||
45 | enumerable: true, | ||
46 | value: properties[prop] | ||
47 | }) | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | |||
52 | const replServer = repl.start({ | ||
53 | prompt: `PeerTube [${cli.version}] (${versionCommitHash})> ` | ||
54 | }) | ||
55 | |||
56 | initContext(replServer)(replServer.context) | ||
57 | replServer.on('reset', initContext(replServer)) | ||
58 | replServer.on('exit', () => process.exit()) | ||
59 | |||
60 | const resetCommand = { | ||
61 | help: 'Reset REPL', | ||
62 | action () { | ||
63 | this.write('.clear\n') | ||
64 | this.displayPrompt() | ||
65 | } | ||
66 | } | ||
67 | replServer.defineCommand('reset', resetCommand) | ||
68 | replServer.defineCommand('r', resetCommand) | ||
69 | |||
70 | } | ||
71 | |||
72 | start().then((data) => { | ||
73 | // do nothing | ||
74 | }).catch((err) => { | ||
75 | console.error(err) | ||
76 | }) | ||
diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts index b17bc4288..ebc62c965 100644 --- a/server/tools/peertube-upload.ts +++ b/server/tools/peertube-upload.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import * as program from 'commander' | 1 | import * as program from 'commander' |
2 | import { access, constants } from 'fs-extra' | 2 | import { access, constants } from 'fs-extra' |
3 | import { isAbsolute } from 'path' | 3 | import { isAbsolute } from 'path' |
4 | import { getClient, login } from '../tests/utils' | 4 | import { getClient, login } from '../../shared/utils' |
5 | import { uploadVideo } from '../tests/utils/index' | 5 | import { uploadVideo } from '../../shared/utils/' |
6 | import { VideoPrivacy } from '../../shared/models/videos' | 6 | import { VideoPrivacy } from '../../shared/models/videos' |
7 | import { netrc, getSettings } from './cli' | 7 | import { netrc, getSettings } from './cli' |
8 | 8 | ||
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts index ad76bafb4..5d3ab2815 100755 --- a/server/tools/peertube.ts +++ b/server/tools/peertube.ts | |||
@@ -17,6 +17,7 @@ program | |||
17 | .command('import-videos', 'import a video from a streaming platform').alias('import') | 17 | .command('import-videos', 'import a video from a streaming platform').alias('import') |
18 | .command('get-access-token', 'get a peertube access token', { noHelp: true }).alias('token') | 18 | .command('get-access-token', 'get a peertube access token', { noHelp: true }).alias('token') |
19 | .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') | 19 | .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') |
20 | .command('repl', 'initiate a REPL to access internals') | ||
20 | 21 | ||
21 | /* Not Yet Implemented */ | 22 | /* Not Yet Implemented */ |
22 | program | 23 | program |
@@ -57,7 +58,7 @@ if (!process.argv.slice(2).length) { | |||
57 | ,"\\/ | 58 | ,"\\/ |
58 | _,.__/"\\/_ (the CLI for red chocobos) | 59 | _,.__/"\\/_ (the CLI for red chocobos) |
59 | / \\) "./, ". | 60 | / \\) "./, ". |
60 | --/---"---" "-) )---- by Chocobozzz et al.`) | 61 | --/---"---" "-) )---- by Chocobozzz et al.\n`) |
61 | } | 62 | } |
62 | 63 | ||
63 | getSettings() | 64 | getSettings() |