diff options
author | Chocobozzz <me@florianbigard.com> | 2019-02-06 12:26:58 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-02-06 12:26:58 +0100 |
commit | 73471b1a52f242e86364ffb077ea6cadb3b07ae2 (patch) | |
tree | 43dbb7748e281f8d80f15326f489cdea10ec857d /server | |
parent | c22419dd265c0c7185bf4197a1cb286eb3d8ebc0 (diff) | |
parent | f5305c04aae14467d6f957b713c5a902275cbb89 (diff) | |
download | PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.tar.gz PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.tar.zst PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.zip |
Merge branch 'release/v1.2.0'
Diffstat (limited to 'server')
262 files changed, 6630 insertions, 4533 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 86ef2aed1..8c0237203 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -14,6 +14,8 @@ import { AccountModel } from '../../models/account/account' | |||
14 | import { VideoModel } from '../../models/video/video' | 14 | import { VideoModel } from '../../models/video/video' |
15 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 15 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
16 | 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' | ||
17 | 19 | ||
18 | const accountsRouter = express.Router() | 20 | const accountsRouter = express.Router() |
19 | 21 | ||
@@ -57,6 +59,11 @@ export { | |||
57 | function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { | 59 | function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { |
58 | const account: AccountModel = res.locals.account | 60 | const account: AccountModel = res.locals.account |
59 | 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 | |||
60 | return res.json(account.toFormattedJSON()) | 67 | return res.json(account.toFormattedJSON()) |
61 | } | 68 | } |
62 | 69 | ||
@@ -74,10 +81,10 @@ async function listVideoAccountChannels (req: express.Request, res: express.Resp | |||
74 | 81 | ||
75 | 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) { |
76 | const account: AccountModel = res.locals.account | 83 | const account: AccountModel = res.locals.account |
77 | const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | 84 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined |
78 | 85 | ||
79 | const resultList = await VideoModel.listForApi({ | 86 | const resultList = await VideoModel.listForApi({ |
80 | actorId, | 87 | followerActorId, |
81 | start: req.query.start, | 88 | start: req.query.start, |
82 | count: req.query.count, | 89 | count: req.query.count, |
83 | sort: req.query.sort, | 90 | sort: req.query.sort, |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 5233e9f68..255026f46 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 { omit, 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' |
@@ -11,6 +11,9 @@ 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 { getServerCommit } 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() |
@@ -61,6 +64,12 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
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: { |
@@ -111,6 +120,11 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
111 | user: { | 120 | user: { |
112 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | 121 | videoQuota: CONFIG.USER.VIDEO_QUOTA, |
113 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | 122 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY |
123 | }, | ||
124 | trending: { | ||
125 | videos: { | ||
126 | intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS | ||
127 | } | ||
114 | } | 128 | } |
115 | } | 129 | } |
116 | 130 | ||
@@ -150,32 +164,10 @@ async function deleteCustomConfig (req: express.Request, res: express.Response, | |||
150 | } | 164 | } |
151 | 165 | ||
152 | async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { | 166 | async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { |
153 | const toUpdate: CustomConfig = req.body | ||
154 | const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig()) | 167 | const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig()) |
155 | 168 | ||
156 | // Force number conversion | 169 | // camelCase to snake_case key + Force number conversion |
157 | toUpdate.cache.previews.size = parseInt('' + toUpdate.cache.previews.size, 10) | 170 | 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 | 171 | ||
180 | await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 }) | 172 | await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 }) |
181 | 173 | ||
@@ -237,12 +229,16 @@ function customConfig (): CustomConfig { | |||
237 | admin: { | 229 | admin: { |
238 | email: CONFIG.ADMIN.EMAIL | 230 | email: CONFIG.ADMIN.EMAIL |
239 | }, | 231 | }, |
232 | contactForm: { | ||
233 | enabled: CONFIG.CONTACT_FORM.ENABLED | ||
234 | }, | ||
240 | user: { | 235 | user: { |
241 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | 236 | videoQuota: CONFIG.USER.VIDEO_QUOTA, |
242 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | 237 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY |
243 | }, | 238 | }, |
244 | transcoding: { | 239 | transcoding: { |
245 | enabled: CONFIG.TRANSCODING.ENABLED, | 240 | enabled: CONFIG.TRANSCODING.ENABLED, |
241 | allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, | ||
246 | threads: CONFIG.TRANSCODING.THREADS, | 242 | threads: CONFIG.TRANSCODING.THREADS, |
247 | resolutions: { | 243 | resolutions: { |
248 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], | 244 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], |
@@ -264,3 +260,20 @@ function customConfig (): CustomConfig { | |||
264 | } | 260 | } |
265 | } | 261 | } |
266 | } | 262 | } |
263 | |||
264 | function convertCustomConfigBody (body: CustomConfig) { | ||
265 | function keyConverter (k: string) { | ||
266 | // Transcoding resolutions exception | ||
267 | if (/^\d{3,4}p$/.exec(k)) return k | ||
268 | |||
269 | return snakeCase(k) | ||
270 | } | ||
271 | |||
272 | function valueConverter (v: any) { | ||
273 | if (isNumeric(v + '')) return parseInt('' + v, 10) | ||
274 | |||
275 | return v | ||
276 | } | ||
277 | |||
278 | return objectConverter(body, keyConverter, valueConverter) | ||
279 | } | ||
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/index.ts b/server/controllers/api/server/index.ts index c08192a8c..814248e5f 100644 --- a/server/controllers/api/server/index.ts +++ b/server/controllers/api/server/index.ts | |||
@@ -3,6 +3,7 @@ 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' | 5 | import { serverBlocklistRouter } from './server-blocklist' |
6 | import { contactRouter } from './contact' | ||
6 | 7 | ||
7 | const serverRouter = express.Router() | 8 | const serverRouter = express.Router() |
8 | 9 | ||
@@ -10,6 +11,7 @@ serverRouter.use('/', serverFollowsRouter) | |||
10 | serverRouter.use('/', serverRedundancyRouter) | 11 | serverRouter.use('/', serverRedundancyRouter) |
11 | serverRouter.use('/', statsRouter) | 12 | serverRouter.use('/', statsRouter) |
12 | serverRouter.use('/', serverBlocklistRouter) | 13 | serverRouter.use('/', serverBlocklistRouter) |
14 | serverRouter.use('/', contactRouter) | ||
13 | 15 | ||
14 | // --------------------------------------------------------------------------- | 16 | // --------------------------------------------------------------------------- |
15 | 17 | ||
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 87fab4a40..dbe0718d4 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -38,6 +38,10 @@ import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../h | |||
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' | 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' | ||
41 | 45 | ||
42 | const auditLogger = auditLoggerFactory('users') | 46 | const auditLogger = auditLoggerFactory('users') |
43 | 47 | ||
@@ -54,7 +58,10 @@ const askSendEmailLimiter = new RateLimit({ | |||
54 | }) | 58 | }) |
55 | 59 | ||
56 | const usersRouter = express.Router() | 60 | const usersRouter = express.Router() |
61 | usersRouter.use('/', myNotificationsRouter) | ||
62 | usersRouter.use('/', mySubscriptionsRouter) | ||
57 | usersRouter.use('/', myBlocklistRouter) | 63 | usersRouter.use('/', myBlocklistRouter) |
64 | usersRouter.use('/', myVideosHistoryRouter) | ||
58 | usersRouter.use('/', meRouter) | 65 | usersRouter.use('/', meRouter) |
59 | 66 | ||
60 | usersRouter.get('/autocomplete', | 67 | usersRouter.get('/autocomplete', |
@@ -209,6 +216,8 @@ async function registerUser (req: express.Request, res: express.Response) { | |||
209 | await sendVerifyUserEmail(user) | 216 | await sendVerifyUserEmail(user) |
210 | } | 217 | } |
211 | 218 | ||
219 | Notifier.Instance.notifyOnNewUserRegistration(user) | ||
220 | |||
212 | return res.type('json').status(204).end() | 221 | return res.type('json').status(204).end() |
213 | } | 222 | } |
214 | 223 | ||
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 82299747d..94a2b8732 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,100 +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 | user | ||
243 | }) | ||
244 | |||
245 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
246 | } | ||
247 | |||
248 | 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) { |
249 | const user = res.locals.oauth.token.User as UserModel | 97 | const user = res.locals.oauth.token.User as UserModel |
250 | const resultList = await VideoModel.listUserVideosForApi( | 98 | const resultList = await VideoModel.listUserVideosForApi( |
@@ -330,6 +178,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr | |||
330 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy | 178 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy |
331 | if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled | 179 | if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled |
332 | 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 | ||
333 | 182 | ||
334 | await sequelizeTypescript.transaction(async t => { | 183 | await sequelizeTypescript.transaction(async t => { |
335 | const userAccount = await AccountModel.load(user.Account.id) | 184 | const userAccount = await AccountModel.load(user.Account.id) |
@@ -348,7 +197,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr | |||
348 | return res.sendStatus(204) | 197 | return res.sendStatus(204) |
349 | } | 198 | } |
350 | 199 | ||
351 | async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { | 200 | async function updateMyAvatar (req: express.Request, res: express.Response) { |
352 | const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] | 201 | const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] |
353 | const user: UserModel = res.locals.oauth.token.user | 202 | const user: UserModel = res.locals.oauth.token.user |
354 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) | 203 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) |
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 9bf3c5fd8..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, |
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index d0c81804b..fe0a95cd5 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts | |||
@@ -22,6 +22,7 @@ import { VideoModel } from '../../../models/video/video' | |||
22 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | 22 | import { VideoAbuseModel } from '../../../models/video/video-abuse' |
23 | import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' | 23 | import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' |
24 | import { UserModel } from '../../../models/account/user' | 24 | import { UserModel } from '../../../models/account/user' |
25 | import { Notifier } from '../../../lib/notifier' | ||
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 3875c8f79..70c1148ba 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -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() |
@@ -119,6 +120,7 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons | |||
119 | }, t) | 120 | }, t) |
120 | }) | 121 | }) |
121 | 122 | ||
123 | Notifier.Instance.notifyOnNewComment(comment) | ||
122 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) | 124 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) |
123 | 125 | ||
124 | return res.json({ | 126 | return res.json({ |
@@ -140,6 +142,7 @@ async function addVideoCommentReply (req: express.Request, res: express.Response | |||
140 | }, t) | 142 | }, t) |
141 | }) | 143 | }) |
142 | 144 | ||
145 | Notifier.Instance.notifyOnNewComment(comment) | ||
143 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) | 146 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) |
144 | 147 | ||
145 | 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 398fd5a7f..98366cd82 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 3d1b2e1a2..2b2dfa7ca 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 { |
@@ -57,27 +56,28 @@ import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-u | |||
57 | import { videoCaptionsRouter } from './captions' | 56 | import { videoCaptionsRouter } from './captions' |
58 | import { videoImportsRouter } from './import' | 57 | import { videoImportsRouter } from './import' |
59 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | 58 | import { resetSequelizeInstance } from '../../../helpers/database-utils' |
60 | import { rename } from 'fs-extra' | 59 | import { move } from 'fs-extra' |
61 | import { watchingRouter } from './watching' | 60 | import { watchingRouter } from './watching' |
61 | import { Notifier } from '../../../lib/notifier' | ||
62 | 62 | ||
63 | const auditLogger = auditLoggerFactory('videos') | 63 | const auditLogger = auditLoggerFactory('videos') |
64 | const videosRouter = express.Router() | 64 | const videosRouter = express.Router() |
65 | 65 | ||
66 | const reqVideoFileAdd = createReqFiles( | 66 | const reqVideoFileAdd = createReqFiles( |
67 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | 67 | [ 'videofile', 'thumbnailfile', 'previewfile' ], |
68 | Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), | 68 | Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), |
69 | { | 69 | { |
70 | videofile: CONFIG.STORAGE.VIDEOS_DIR, | 70 | videofile: CONFIG.STORAGE.TMP_DIR, |
71 | thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, | 71 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, |
72 | previewfile: CONFIG.STORAGE.PREVIEWS_DIR | 72 | previewfile: CONFIG.STORAGE.TMP_DIR |
73 | } | 73 | } |
74 | ) | 74 | ) |
75 | const reqVideoFileUpdate = createReqFiles( | 75 | const reqVideoFileUpdate = createReqFiles( |
76 | [ 'thumbnailfile', 'previewfile' ], | 76 | [ 'thumbnailfile', 'previewfile' ], |
77 | IMAGE_MIMETYPE_EXT, | 77 | MIMETYPES.IMAGE.MIMETYPE_EXT, |
78 | { | 78 | { |
79 | thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, | 79 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, |
80 | previewfile: CONFIG.STORAGE.PREVIEWS_DIR | 80 | previewfile: CONFIG.STORAGE.TMP_DIR |
81 | } | 81 | } |
82 | ) | 82 | ) |
83 | 83 | ||
@@ -208,7 +208,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
208 | // Move physical file | 208 | // Move physical file |
209 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR | 209 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR |
210 | const destination = join(videoDir, video.getVideoFilename(videoFile)) | 210 | const destination = join(videoDir, video.getVideoFilename(videoFile)) |
211 | await rename(videoPhysicalFile.path, destination) | 211 | await move(videoPhysicalFile.path, destination) |
212 | // This is important in case if there is another attempt in the retry process | 212 | // This is important in case if there is another attempt in the retry process |
213 | videoPhysicalFile.filename = video.getVideoFilename(videoFile) | 213 | videoPhysicalFile.filename = video.getVideoFilename(videoFile) |
214 | videoPhysicalFile.path = destination | 214 | videoPhysicalFile.path = destination |
@@ -271,6 +271,8 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
271 | return videoCreated | 271 | return videoCreated |
272 | }) | 272 | }) |
273 | 273 | ||
274 | Notifier.Instance.notifyOnNewVideo(videoCreated) | ||
275 | |||
274 | if (video.state === VideoState.TO_TRANSCODE) { | 276 | if (video.state === VideoState.TO_TRANSCODE) { |
275 | // Put uuid because we don't have id auto incremented for now | 277 | // Put uuid because we don't have id auto incremented for now |
276 | const dataInput = { | 278 | const dataInput = { |
@@ -295,6 +297,7 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
295 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) | 297 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) |
296 | const videoInfoToUpdate: VideoUpdate = req.body | 298 | const videoInfoToUpdate: VideoUpdate = req.body |
297 | const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE | 299 | const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE |
300 | const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED | ||
298 | 301 | ||
299 | // Process thumbnail or create it from the video | 302 | // Process thumbnail or create it from the video |
300 | if (req.files && req.files['thumbnailfile']) { | 303 | if (req.files && req.files['thumbnailfile']) { |
@@ -309,10 +312,8 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
309 | } | 312 | } |
310 | 313 | ||
311 | try { | 314 | try { |
312 | await sequelizeTypescript.transaction(async t => { | 315 | const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => { |
313 | const sequelizeOptions = { | 316 | const sequelizeOptions = { transaction: t } |
314 | transaction: t | ||
315 | } | ||
316 | const oldVideoChannel = videoInstance.VideoChannel | 317 | const oldVideoChannel = videoInstance.VideoChannel |
317 | 318 | ||
318 | if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name) | 319 | if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name) |
@@ -363,7 +364,11 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
363 | } | 364 | } |
364 | 365 | ||
365 | const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE | 366 | const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE |
366 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | 367 | |
368 | // Don't send update if the video was unfederated | ||
369 | if (!videoInstanceUpdated.VideoBlacklist || videoInstanceUpdated.VideoBlacklist.unfederated === false) { | ||
370 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | ||
371 | } | ||
367 | 372 | ||
368 | auditLogger.update( | 373 | auditLogger.update( |
369 | getAuditIdFromRes(res), | 374 | getAuditIdFromRes(res), |
@@ -371,7 +376,13 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
371 | oldVideoAuditView | 376 | oldVideoAuditView |
372 | ) | 377 | ) |
373 | logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) | 378 | logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) |
379 | |||
380 | return videoInstanceUpdated | ||
374 | }) | 381 | }) |
382 | |||
383 | if (wasUnlistedVideo || wasPrivateVideo) { | ||
384 | Notifier.Instance.notifyOnNewVideo(videoInstanceUpdated) | ||
385 | } | ||
375 | } catch (err) { | 386 | } catch (err) { |
376 | // Force fields we want to update | 387 | // Force fields we want to update |
377 | // If the transaction is retried, sequelize will think the object has not changed | 388 | // If the transaction is retried, sequelize will think the object has not changed |
@@ -388,7 +399,7 @@ function getVideo (req: express.Request, res: express.Response) { | |||
388 | const videoInstance = res.locals.video | 399 | const videoInstance = res.locals.video |
389 | 400 | ||
390 | if (videoInstance.isOutdated()) { | 401 | if (videoInstance.isOutdated()) { |
391 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoInstance.url } }) | 402 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoInstance.url } }) |
392 | .catch(err => logger.error('Cannot create AP refresher job for video %s.', videoInstance.url, { err })) | 403 | .catch(err => logger.error('Cannot create AP refresher job for video %s.', videoInstance.url, { err })) |
393 | } | 404 | } |
394 | 405 | ||
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 ccb9b6029..960085af1 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -56,7 +56,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res | |||
56 | 56 | ||
57 | // Adding video items to the feed, one at a time | 57 | // Adding video items to the feed, one at a time |
58 | comments.forEach(comment => { | 58 | comments.forEach(comment => { |
59 | const link = CONFIG.WEBSERVER.URL + '/videos/watch/' + comment.Video.uuid + ';threadId=' + comment.getThreadId() | 59 | const link = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() |
60 | 60 | ||
61 | feed.addItem({ | 61 | feed.addItem({ |
62 | 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..4fd58f70c 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -34,13 +34,18 @@ 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) |
@@ -131,6 +136,12 @@ staticRouter.use('/.well-known/dnt/', | |||
131 | } | 136 | } |
132 | ) | 137 | ) |
133 | 138 | ||
139 | staticRouter.use('/.well-known/change-password', | ||
140 | (_, res: express.Response) => { | ||
141 | res.redirect('/my-account/settings') | ||
142 | } | ||
143 | ) | ||
144 | |||
134 | // --------------------------------------------------------------------------- | 145 | // --------------------------------------------------------------------------- |
135 | 146 | ||
136 | export { | 147 | export { |
diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts index 9bc7586d1..1deb8c402 100644 --- a/server/controllers/tracker.ts +++ b/server/controllers/tracker.ts | |||
@@ -6,6 +6,7 @@ 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' | ||
9 | 10 | ||
10 | const TrackerServer = bitTorrentTracker.Server | 11 | const TrackerServer = bitTorrentTracker.Server |
11 | 12 | ||
@@ -59,16 +60,26 @@ const onHttpRequest = trackerServer.onHttpRequest.bind(trackerServer) | |||
59 | trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' })) | 60 | trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' })) |
60 | trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' })) | 61 | trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' })) |
61 | 62 | ||
62 | function createWebsocketServer (app: express.Application) { | 63 | function createWebsocketTrackerServer (app: express.Application) { |
63 | const server = http.createServer(app) | 64 | const server = http.createServer(app) |
64 | const wss = new WebSocketServer({ server: server, path: '/tracker/socket' }) | 65 | const wss = new WebSocketServer({ noServer: true }) |
66 | |||
65 | wss.on('connection', function (ws, req) { | 67 | wss.on('connection', function (ws, req) { |
66 | const ip = proxyAddr(req, CONFIG.TRUST_PROXY) | 68 | ws['ip'] = proxyAddr(req, CONFIG.TRUST_PROXY) |
67 | ws['ip'] = ip | ||
68 | 69 | ||
69 | trackerServer.onWebSocketConnection(ws) | 70 | trackerServer.onWebSocketConnection(ws) |
70 | }) | 71 | }) |
71 | 72 | ||
73 | server.on('upgrade', (request, socket, head) => { | ||
74 | const pathname = parse(request.url).pathname | ||
75 | |||
76 | if (pathname === '/tracker/socket') { | ||
77 | wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request)) | ||
78 | } | ||
79 | |||
80 | // Don't destroy socket, we have Socket.IO too | ||
81 | }) | ||
82 | |||
72 | return server | 83 | return server |
73 | } | 84 | } |
74 | 85 | ||
@@ -76,7 +87,7 @@ function createWebsocketServer (app: express.Application) { | |||
76 | 87 | ||
77 | export { | 88 | export { |
78 | trackerRouter, | 89 | trackerRouter, |
79 | createWebsocketServer | 90 | createWebsocketTrackerServer |
80 | } | 91 | } |
81 | 92 | ||
82 | // --------------------------------------------------------------------------- | 93 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index 79b76fa0b..f1430055f 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts | |||
@@ -106,7 +106,7 @@ function buildSignedActivity (byActor: ActorModel, data: Object) { | |||
106 | return signJsonLDObject(byActor, activity) as Promise<Activity> | 106 | return signJsonLDObject(byActor, activity) as Promise<Activity> |
107 | } | 107 | } |
108 | 108 | ||
109 | function getAPUrl (activity: string | { id: string }) { | 109 | function getAPId (activity: string | { id: string }) { |
110 | if (typeof activity === 'string') return activity | 110 | if (typeof activity === 'string') return activity |
111 | 111 | ||
112 | return activity.id | 112 | return activity.id |
@@ -123,7 +123,7 @@ function checkUrlsSameHost (url1: string, url2: string) { | |||
123 | 123 | ||
124 | export { | 124 | export { |
125 | checkUrlsSameHost, | 125 | checkUrlsSameHost, |
126 | getAPUrl, | 126 | getAPId, |
127 | activityPubContextify, | 127 | activityPubContextify, |
128 | activityPubCollectionPagination, | 128 | activityPubCollectionPagination, |
129 | buildSignedActivity | 129 | 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 84e33c0e9..3fb824e36 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -11,6 +11,25 @@ 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, |
@@ -235,6 +254,7 @@ export { | |||
235 | isTestInstance, | 254 | isTestInstance, |
236 | isProdInstance, | 255 | isProdInstance, |
237 | 256 | ||
257 | objectConverter, | ||
238 | root, | 258 | root, |
239 | escapeHTML, | 259 | escapeHTML, |
240 | pageToStartAndCount, | 260 | pageToStartAndCount, |
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..e2bd0c55e 100644 --- a/server/helpers/custom-validators/activitypub/cache-file.ts +++ b/server/helpers/custom-validators/activitypub/cache-file.ts | |||
@@ -1,18 +1,8 @@ | |||
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' && |
@@ -22,7 +12,5 @@ function isCacheFileObjectValid (object: CacheFileObject) { | |||
22 | } | 12 | } |
23 | 13 | ||
24 | export { | 14 | export { |
25 | isCacheFileUpdateActivityValid, | ||
26 | isCacheFileCreateActivityValid, | ||
27 | isCacheFileObjectValid | 15 | isCacheFileObjectValid |
28 | } | 16 | } |
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 95fe824b9..0f34aab21 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -14,27 +14,11 @@ import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from | |||
14 | import { VideoState } from '../../../../shared/models/videos' | 14 | import { VideoState } from '../../../../shared/models/videos' |
15 | import { isVideoAbuseReasonValid } from '../video-abuses' | 15 | import { isVideoAbuseReasonValid } from '../video-abuses' |
16 | 16 | ||
17 | function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { | ||
18 | return isBaseActivityValid(activity, 'Create') && | ||
19 | sanitizeAndCheckVideoTorrentObject(activity.object) | ||
20 | } | ||
21 | |||
22 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { | 17 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { |
23 | return isBaseActivityValid(activity, 'Update') && | 18 | return isBaseActivityValid(activity, 'Update') && |
24 | sanitizeAndCheckVideoTorrentObject(activity.object) | 19 | sanitizeAndCheckVideoTorrentObject(activity.object) |
25 | } | 20 | } |
26 | 21 | ||
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) { | 22 | function isActivityPubVideoDurationValid (value: string) { |
39 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | 23 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration |
40 | return exists(value) && | 24 | return exists(value) && |
@@ -103,11 +87,8 @@ function isRemoteVideoUrlValid (url: any) { | |||
103 | // --------------------------------------------------------------------------- | 87 | // --------------------------------------------------------------------------- |
104 | 88 | ||
105 | export { | 89 | export { |
106 | sanitizeAndCheckVideoTorrentCreateActivity, | ||
107 | sanitizeAndCheckVideoTorrentUpdateActivity, | 90 | sanitizeAndCheckVideoTorrentUpdateActivity, |
108 | isVideoTorrentDeleteActivityValid, | ||
109 | isRemoteStringIdentifierValid, | 91 | isRemoteStringIdentifierValid, |
110 | isVideoFlagValid, | ||
111 | sanitizeAndCheckVideoTorrentObject, | 92 | sanitizeAndCheckVideoTorrentObject, |
112 | isRemoteVideoUrlValid | 93 | isRemoteVideoUrlValid |
113 | } | 94 | } |
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..b6f0ebe6f 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -9,6 +9,10 @@ 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 | |||
12 | function isDateValid (value: string) { | 16 | function isDateValid (value: string) { |
13 | return exists(value) && validator.isISO8601(value) | 17 | return exists(value) && validator.isISO8601(value) |
14 | } | 18 | } |
@@ -78,6 +82,7 @@ function isFileValid ( | |||
78 | 82 | ||
79 | export { | 83 | export { |
80 | exists, | 84 | exists, |
85 | isNotEmptyIntArray, | ||
81 | isArray, | 86 | isArray, |
82 | isIdValid, | 87 | isIdValid, |
83 | isUUIDValid, | 88 | 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 1cb5e5b0f..80652b479 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -46,6 +46,10 @@ function isUserWebTorrentEnabledValid (value: any) { | |||
46 | return isBooleanValid(value) | 46 | return isBooleanValid(value) |
47 | } | 47 | } |
48 | 48 | ||
49 | function isUserVideosHistoryEnabledValid (value: any) { | ||
50 | return isBooleanValid(value) | ||
51 | } | ||
52 | |||
49 | function isUserAutoPlayVideoValid (value: any) { | 53 | function isUserAutoPlayVideoValid (value: any) { |
50 | return isBooleanValid(value) | 54 | return isBooleanValid(value) |
51 | } | 55 | } |
@@ -73,6 +77,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | | |||
73 | // --------------------------------------------------------------------------- | 77 | // --------------------------------------------------------------------------- |
74 | 78 | ||
75 | export { | 79 | export { |
80 | isUserVideosHistoryEnabledValid, | ||
76 | isUserBlockedValid, | 81 | isUserBlockedValid, |
77 | isUserPasswordValid, | 82 | isUserPasswordValid, |
78 | isUserBlockedReasonValid, | 83 | isUserBlockedReasonValid, |
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 a13b09ac8..95e256b8f 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -5,10 +5,9 @@ import 'multer' | |||
5 | import * as validator from 'validator' | 5 | import * as validator from 'validator' |
6 | import { UserRight, VideoFilter, 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 |
@@ -83,10 +82,15 @@ function isVideoRatingTypeValid (value: string) { | |||
83 | 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 |
84 | } | 83 | } |
85 | 84 | ||
86 | const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) | 85 | function isVideoFileExtnameValid (value: string) { |
87 | const videoFileTypesRegex = videoFileTypes.join('|') | 86 | return exists(value) && MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined |
87 | } | ||
88 | 88 | ||
89 | 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 | |||
90 | return isFileValid(files, videoFileTypesRegex, 'videofile', null) | 94 | return isFileValid(files, videoFileTypesRegex, 'videofile', null) |
91 | } | 95 | } |
92 | 96 | ||
@@ -221,6 +225,7 @@ export { | |||
221 | isVideoStateValid, | 225 | isVideoStateValid, |
222 | isVideoViewsValid, | 226 | isVideoViewsValid, |
223 | isVideoRatingTypeValid, | 227 | isVideoRatingTypeValid, |
228 | isVideoFileExtnameValid, | ||
224 | isVideoDurationValid, | 229 | isVideoDurationValid, |
225 | isVideoTagValid, | 230 | isVideoTagValid, |
226 | isVideoPrivacyValid, | 231 | isVideoPrivacyValid, |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index 162fe2244..9a72ee96d 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -7,12 +7,12 @@ import { extname } from 'path' | |||
7 | import { isArray } from './custom-validators/misc' | 7 | import { isArray } from './custom-validators/misc' |
8 | import { UserModel } from '../models/account/user' | 8 | import { UserModel } from '../models/account/user' |
9 | 9 | ||
10 | function buildNSFWFilter (res: express.Response, paramNSFW?: string) { | 10 | function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { |
11 | if (paramNSFW === 'true') return true | 11 | if (paramNSFW === 'true') return true |
12 | if (paramNSFW === 'false') return false | 12 | if (paramNSFW === 'false') return false |
13 | if (paramNSFW === 'both') return undefined | 13 | if (paramNSFW === 'both') return undefined |
14 | 14 | ||
15 | if (res.locals.oauth) { | 15 | if (res && res.locals.oauth) { |
16 | const user: UserModel = res.locals.oauth.token.User | 16 | const user: UserModel = res.locals.oauth.token.User |
17 | 17 | ||
18 | // User does not want NSFW videos | 18 | // User does not want NSFW videos |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index b59e7e40e..132f4690e 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -41,7 +41,7 @@ async function getVideoFileResolution (path: string) { | |||
41 | async function getVideoFileFPS (path: string) { | 41 | async function getVideoFileFPS (path: string) { |
42 | const videoStream = await getVideoFileStream(path) | 42 | const videoStream = await getVideoFileStream(path) |
43 | 43 | ||
44 | for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) { | 44 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { |
45 | const valuesText: string = videoStream[key] | 45 | const valuesText: string = videoStream[key] |
46 | if (!valuesText) continue | 46 | if (!valuesText) continue |
47 | 47 | ||
@@ -184,7 +184,7 @@ function getVideoFileStream (path: string) { | |||
184 | if (err) return rej(err) | 184 | if (err) return rej(err) |
185 | 185 | ||
186 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') | 186 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') |
187 | if (!videoStream) throw new Error('Cannot find video stream of ' + path) | 187 | if (!videoStream) return rej(new Error('Cannot find video stream of ' + path)) |
188 | 188 | ||
189 | return res(videoStream) | 189 | return res(videoStream) |
190 | }) | 190 | }) |
@@ -328,10 +328,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol | |||
328 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] | 328 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] |
329 | let bitrate: number | 329 | let bitrate: number |
330 | if (audio.bitrate[ audioCodecName ]) { | 330 | if (audio.bitrate[ audioCodecName ]) { |
331 | bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | 331 | localCommand = localCommand.audioCodec('aac') |
332 | 332 | ||
333 | if (bitrate === -1) localCommand = localCommand.audioCodec('copy') | 333 | bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) |
334 | else if (bitrate !== undefined) localCommand = localCommand.audioBitrate(bitrate) | 334 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) |
335 | } | 335 | } |
336 | } | 336 | } |
337 | 337 | ||
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index da3285b13..e43ea3f1d 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import * as sharp from 'sharp' | 2 | import * as sharp from 'sharp' |
3 | import { move, 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 }, |
@@ -11,14 +12,11 @@ async function processImage ( | |||
11 | throw new Error('Sharp needs an input path different that the output path.') | 12 | throw new Error('Sharp needs an input path different that the output path.') |
12 | } | 13 | } |
13 | 14 | ||
14 | const sharpInstance = sharp(physicalFile.path) | 15 | logger.debug('Processing image %s to %s.', physicalFile.path, destination) |
15 | const metadata = await sharpInstance.metadata() | ||
16 | 16 | ||
17 | // No need to resize | 17 | // Avoid sharp cache |
18 | if (metadata.width === newSize.width && metadata.height === newSize.height) { | 18 | const buf = await readFile(physicalFile.path) |
19 | await move(physicalFile.path, destination, { overwrite: true }) | 19 | const sharpInstance = sharp(buf) |
20 | return | ||
21 | } | ||
22 | 20 | ||
23 | await remove(destination) | 21 | await remove(destination) |
24 | 22 | ||
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 805930a9f..3fc776f1a 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -1,8 +1,9 @@ | |||
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' | 5 | import { processImage } from './image-utils' |
6 | import { join } from 'path' | ||
6 | 7 | ||
7 | function doRequest <T> ( | 8 | function doRequest <T> ( |
8 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } | 9 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } |
@@ -28,11 +29,11 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U | |||
28 | }) | 29 | }) |
29 | } | 30 | } |
30 | 31 | ||
31 | async function downloadImage (url: string, destPath: string, size: { width: number, height: number }) { | 32 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { |
32 | const tmpPath = destPath + '.tmp' | 33 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) |
33 | |||
34 | await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) | 34 | await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) |
35 | 35 | ||
36 | const destPath = join(destDir, destName) | ||
36 | await processImage({ path: tmpPath }, destPath, size) | 37 | await processImage({ path: tmpPath }, destPath, size) |
37 | } | 38 | } |
38 | 39 | ||
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 5c9d6fe2f..3c3406e38 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -7,6 +7,7 @@ import { join } from 'path' | |||
7 | import { Instance as ParseTorrent } from 'parse-torrent' | 7 | import { Instance as ParseTorrent } from 'parse-torrent' |
8 | import { remove } from 'fs-extra' | 8 | import { remove } from 'fs-extra' |
9 | import * as memoizee from 'memoizee' | 9 | import * as memoizee from 'memoizee' |
10 | import { isArray } from './custom-validators/misc' | ||
10 | 11 | ||
11 | function deleteFileAsync (path: string) { | 12 | function deleteFileAsync (path: string) { |
12 | remove(path) | 13 | remove(path) |
@@ -19,10 +20,7 @@ async function generateRandomString (size: number) { | |||
19 | return raw.toString('hex') | 20 | return raw.toString('hex') |
20 | } | 21 | } |
21 | 22 | ||
22 | interface FormattableToJSON { | 23 | interface FormattableToJSON { toFormattedJSON (args?: any) } |
23 | toFormattedJSON (args?: any) | ||
24 | } | ||
25 | |||
26 | function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) { | 24 | function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) { |
27 | const formattedObjects: U[] = [] | 25 | const formattedObjects: U[] = [] |
28 | 26 | ||
@@ -46,11 +44,11 @@ const getServerActor = memoizee(async function () { | |||
46 | return actor | 44 | return actor |
47 | }) | 45 | }) |
48 | 46 | ||
49 | function generateVideoTmpPath (target: string | ParseTorrent) { | 47 | function generateVideoImportTmpPath (target: string | ParseTorrent) { |
50 | const id = typeof target === 'string' ? target : target.infoHash | 48 | const id = typeof target === 'string' ? target : target.infoHash |
51 | 49 | ||
52 | const hash = sha256(id) | 50 | const hash = sha256(id) |
53 | return join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4') | 51 | return join(CONFIG.STORAGE.TMP_DIR, hash + '-import.mp4') |
54 | } | 52 | } |
55 | 53 | ||
56 | function getSecureTorrentName (originalName: string) { | 54 | function getSecureTorrentName (originalName: string) { |
@@ -103,6 +101,6 @@ export { | |||
103 | getSecureTorrentName, | 101 | getSecureTorrentName, |
104 | getServerActor, | 102 | getServerActor, |
105 | getServerCommit, | 103 | getServerCommit, |
106 | generateVideoTmpPath, | 104 | generateVideoImportTmpPath, |
107 | getUUIDFromFilename | 105 | getUUIDFromFilename |
108 | } | 106 | } |
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 2a5663042..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' |
@@ -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 9dfb5d68c..7905d9ffa 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', | ||
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', |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index aa243859c..6f3ebb9aa 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -16,7 +16,7 @@ let config: IConfig = require('config') | |||
16 | 16 | ||
17 | // --------------------------------------------------------------------------- | 17 | // --------------------------------------------------------------------------- |
18 | 18 | ||
19 | const LAST_MIGRATION_VERSION = 290 | 19 | const LAST_MIGRATION_VERSION = 325 |
20 | 20 | ||
21 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
22 | 22 | ||
@@ -50,7 +50,9 @@ const SORTABLE_COLUMNS = { | |||
50 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], | 50 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], |
51 | 51 | ||
52 | ACCOUNTS_BLOCKLIST: [ 'createdAt' ], | 52 | ACCOUNTS_BLOCKLIST: [ 'createdAt' ], |
53 | SERVERS_BLOCKLIST: [ 'createdAt' ] | 53 | SERVERS_BLOCKLIST: [ 'createdAt' ], |
54 | |||
55 | USER_NOTIFICATIONS: [ 'createdAt' ] | ||
54 | } | 56 | } |
55 | 57 | ||
56 | const OAUTH_LIFETIME = { | 58 | const OAUTH_LIFETIME = { |
@@ -61,6 +63,7 @@ const OAUTH_LIFETIME = { | |||
61 | const ROUTE_CACHE_LIFETIME = { | 63 | const ROUTE_CACHE_LIFETIME = { |
62 | FEEDS: '15 minutes', | 64 | FEEDS: '15 minutes', |
63 | ROBOTS: '2 hours', | 65 | ROBOTS: '2 hours', |
66 | SITEMAP: '1 day', | ||
64 | SECURITYTXT: '2 hours', | 67 | SECURITYTXT: '2 hours', |
65 | NODEINFO: '10 minutes', | 68 | NODEINFO: '10 minutes', |
66 | DNT_POLICY: '1 week', | 69 | DNT_POLICY: '1 week', |
@@ -143,7 +146,7 @@ const VIDEO_IMPORT_TIMEOUT = 1000 * 3600 // 1 hour | |||
143 | 146 | ||
144 | // 1 hour | 147 | // 1 hour |
145 | let SCHEDULER_INTERVALS_MS = { | 148 | let SCHEDULER_INTERVALS_MS = { |
146 | badActorFollow: 60000 * 60, // 1 hour | 149 | actorFollowScores: 60000 * 60, // 1 hour |
147 | removeOldJobs: 60000 * 60, // 1 hour | 150 | removeOldJobs: 60000 * 60, // 1 hour |
148 | updateVideos: 60000, // 1 minute | 151 | updateVideos: 60000, // 1 minute |
149 | youtubeDLUpdate: 60000 * 60 * 24 // 1 day | 152 | youtubeDLUpdate: 60000 * 60 * 24 // 1 day |
@@ -185,9 +188,11 @@ const CONFIG = { | |||
185 | FROM_ADDRESS: config.get<string>('smtp.from_address') | 188 | FROM_ADDRESS: config.get<string>('smtp.from_address') |
186 | }, | 189 | }, |
187 | STORAGE: { | 190 | STORAGE: { |
191 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), | ||
188 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), | 192 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), |
189 | LOG_DIR: buildPath(config.get<string>('storage.logs')), | 193 | LOG_DIR: buildPath(config.get<string>('storage.logs')), |
190 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), | 194 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), |
195 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), | ||
191 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), | 196 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), |
192 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), | 197 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), |
193 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), | 198 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), |
@@ -226,6 +231,9 @@ const CONFIG = { | |||
226 | ADMIN: { | 231 | ADMIN: { |
227 | get EMAIL () { return config.get<string>('admin.email') } | 232 | get EMAIL () { return config.get<string>('admin.email') } |
228 | }, | 233 | }, |
234 | CONTACT_FORM: { | ||
235 | get ENABLED () { return config.get<boolean>('contact_form.enabled') } | ||
236 | }, | ||
229 | SIGNUP: { | 237 | SIGNUP: { |
230 | get ENABLED () { return config.get<boolean>('signup.enabled') }, | 238 | get ENABLED () { return config.get<boolean>('signup.enabled') }, |
231 | get LIMIT () { return config.get<number>('signup.limit') }, | 239 | get LIMIT () { return config.get<number>('signup.limit') }, |
@@ -243,6 +251,7 @@ const CONFIG = { | |||
243 | }, | 251 | }, |
244 | TRANSCODING: { | 252 | TRANSCODING: { |
245 | get ENABLED () { return config.get<boolean>('transcoding.enabled') }, | 253 | get ENABLED () { return config.get<boolean>('transcoding.enabled') }, |
254 | get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') }, | ||
246 | get THREADS () { return config.get<number>('transcoding.threads') }, | 255 | get THREADS () { return config.get<number>('transcoding.threads') }, |
247 | RESOLUTIONS: { | 256 | RESOLUTIONS: { |
248 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, | 257 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, |
@@ -286,6 +295,7 @@ const CONFIG = { | |||
286 | get SECURITYTXT_CONTACT () { return config.get<string>('admin.email') } | 295 | get SECURITYTXT_CONTACT () { return config.get<string>('admin.email') } |
287 | }, | 296 | }, |
288 | SERVICES: { | 297 | SERVICES: { |
298 | get 'CSP-LOGGER' () { return config.get<string>('services.csp-logger') }, | ||
289 | TWITTER: { | 299 | TWITTER: { |
290 | get USERNAME () { return config.get<string>('services.twitter.username') }, | 300 | get USERNAME () { return config.get<string>('services.twitter.username') }, |
291 | get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') } | 301 | get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') } |
@@ -295,25 +305,25 @@ const CONFIG = { | |||
295 | 305 | ||
296 | // --------------------------------------------------------------------------- | 306 | // --------------------------------------------------------------------------- |
297 | 307 | ||
298 | const CONSTRAINTS_FIELDS = { | 308 | let CONSTRAINTS_FIELDS = { |
299 | USERS: { | 309 | USERS: { |
300 | NAME: { min: 3, max: 120 }, // Length | 310 | NAME: { min: 1, max: 120 }, // Length |
301 | DESCRIPTION: { min: 3, max: 1000 }, // Length | 311 | DESCRIPTION: { min: 3, max: 1000 }, // Length |
302 | USERNAME: { min: 3, max: 20 }, // Length | 312 | USERNAME: { min: 1, max: 50 }, // Length |
303 | PASSWORD: { min: 6, max: 255 }, // Length | 313 | PASSWORD: { min: 6, max: 255 }, // Length |
304 | VIDEO_QUOTA: { min: -1 }, | 314 | VIDEO_QUOTA: { min: -1 }, |
305 | VIDEO_QUOTA_DAILY: { min: -1 }, | 315 | VIDEO_QUOTA_DAILY: { min: -1 }, |
306 | BLOCKED_REASON: { min: 3, max: 250 } // Length | 316 | BLOCKED_REASON: { min: 3, max: 250 } // Length |
307 | }, | 317 | }, |
308 | VIDEO_ABUSES: { | 318 | VIDEO_ABUSES: { |
309 | REASON: { min: 2, max: 300 }, // Length | 319 | REASON: { min: 2, max: 3000 }, // Length |
310 | MODERATION_COMMENT: { min: 2, max: 300 } // Length | 320 | MODERATION_COMMENT: { min: 2, max: 3000 } // Length |
311 | }, | 321 | }, |
312 | VIDEO_BLACKLIST: { | 322 | VIDEO_BLACKLIST: { |
313 | REASON: { min: 2, max: 300 } // Length | 323 | REASON: { min: 2, max: 300 } // Length |
314 | }, | 324 | }, |
315 | VIDEO_CHANNELS: { | 325 | VIDEO_CHANNELS: { |
316 | NAME: { min: 3, max: 120 }, // Length | 326 | NAME: { min: 1, max: 120 }, // Length |
317 | DESCRIPTION: { min: 3, max: 1000 }, // Length | 327 | DESCRIPTION: { min: 3, max: 1000 }, // Length |
318 | SUPPORT: { min: 3, max: 1000 }, // Length | 328 | SUPPORT: { min: 3, max: 1000 }, // Length |
319 | URL: { min: 3, max: 2000 } // Length | 329 | URL: { min: 3, max: 2000 } // Length |
@@ -354,7 +364,7 @@ const CONSTRAINTS_FIELDS = { | |||
354 | max: 2 * 1024 * 1024 // 2MB | 364 | max: 2 * 1024 * 1024 // 2MB |
355 | } | 365 | } |
356 | }, | 366 | }, |
357 | EXTNAME: [ '.mp4', '.ogv', '.webm' ], | 367 | EXTNAME: buildVideosExtname(), |
358 | INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 | 368 | INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 |
359 | DURATION: { min: 0 }, // Number | 369 | DURATION: { min: 0 }, // Number |
360 | TAGS: { min: 0, max: 5 }, // Number of total tags | 370 | TAGS: { min: 0, max: 5 }, // Number of total tags |
@@ -387,6 +397,10 @@ const CONSTRAINTS_FIELDS = { | |||
387 | }, | 397 | }, |
388 | VIDEO_SHARE: { | 398 | VIDEO_SHARE: { |
389 | URL: { min: 3, max: 2000 } // Length | 399 | URL: { min: 3, max: 2000 } // Length |
400 | }, | ||
401 | CONTACT_FORM: { | ||
402 | FROM_NAME: { min: 1, max: 120 }, // Length | ||
403 | BODY: { min: 3, max: 5000 } // Length | ||
390 | } | 404 | } |
391 | } | 405 | } |
392 | 406 | ||
@@ -402,6 +416,8 @@ const RATES_LIMIT = { | |||
402 | } | 416 | } |
403 | 417 | ||
404 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour | 418 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour |
419 | let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour | ||
420 | |||
405 | const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { | 421 | const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { |
406 | MIN: 10, | 422 | MIN: 10, |
407 | AVERAGE: 30, | 423 | AVERAGE: 30, |
@@ -477,27 +493,31 @@ const VIDEO_ABUSE_STATES = { | |||
477 | [VideoAbuseState.ACCEPTED]: 'Accepted' | 493 | [VideoAbuseState.ACCEPTED]: 'Accepted' |
478 | } | 494 | } |
479 | 495 | ||
480 | const VIDEO_MIMETYPE_EXT = { | 496 | const MIMETYPES = { |
481 | 'video/webm': '.webm', | 497 | VIDEO: { |
482 | 'video/ogg': '.ogv', | 498 | MIMETYPE_EXT: buildVideoMimetypeExt(), |
483 | 'video/mp4': '.mp4' | 499 | EXT_MIMETYPE: null as { [ id: string ]: string } |
484 | } | 500 | }, |
485 | const VIDEO_EXT_MIMETYPE = invert(VIDEO_MIMETYPE_EXT) | 501 | IMAGE: { |
486 | 502 | MIMETYPE_EXT: { | |
487 | const IMAGE_MIMETYPE_EXT = { | 503 | 'image/png': '.png', |
488 | 'image/png': '.png', | 504 | 'image/jpg': '.jpg', |
489 | 'image/jpg': '.jpg', | 505 | 'image/jpeg': '.jpg' |
490 | 'image/jpeg': '.jpg' | 506 | } |
491 | } | 507 | }, |
492 | 508 | VIDEO_CAPTIONS: { | |
493 | const VIDEO_CAPTIONS_MIMETYPE_EXT = { | 509 | MIMETYPE_EXT: { |
494 | 'text/vtt': '.vtt', | 510 | 'text/vtt': '.vtt', |
495 | 'application/x-subrip': '.srt' | 511 | 'application/x-subrip': '.srt' |
496 | } | 512 | } |
497 | 513 | }, | |
498 | const TORRENT_MIMETYPE_EXT = { | 514 | TORRENT: { |
499 | 'application/x-bittorrent': '.torrent' | 515 | MIMETYPE_EXT: { |
516 | 'application/x-bittorrent': '.torrent' | ||
517 | } | ||
518 | } | ||
500 | } | 519 | } |
520 | MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) | ||
501 | 521 | ||
502 | // --------------------------------------------------------------------------- | 522 | // --------------------------------------------------------------------------- |
503 | 523 | ||
@@ -523,7 +543,7 @@ const ACTIVITY_PUB = { | |||
523 | COLLECTION_ITEMS_PER_PAGE: 10, | 543 | COLLECTION_ITEMS_PER_PAGE: 10, |
524 | FETCH_PAGE_LIMIT: 100, | 544 | FETCH_PAGE_LIMIT: 100, |
525 | URL_MIME_TYPES: { | 545 | URL_MIME_TYPES: { |
526 | VIDEO: Object.keys(VIDEO_MIMETYPE_EXT), | 546 | VIDEO: Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT), |
527 | TORRENT: [ 'application/x-bittorrent' ], | 547 | TORRENT: [ 'application/x-bittorrent' ], |
528 | MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] | 548 | MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] |
529 | }, | 549 | }, |
@@ -569,6 +589,7 @@ const STATIC_PATHS = { | |||
569 | THUMBNAILS: '/static/thumbnails/', | 589 | THUMBNAILS: '/static/thumbnails/', |
570 | TORRENTS: '/static/torrents/', | 590 | TORRENTS: '/static/torrents/', |
571 | WEBSEED: '/static/webseed/', | 591 | WEBSEED: '/static/webseed/', |
592 | REDUNDANCY: '/static/redundancy/', | ||
572 | AVATARS: '/static/avatars/', | 593 | AVATARS: '/static/avatars/', |
573 | VIDEO_CAPTIONS: '/static/video-captions/' | 594 | VIDEO_CAPTIONS: '/static/video-captions/' |
574 | } | 595 | } |
@@ -665,7 +686,7 @@ if (isTestInstance() === true) { | |||
665 | 686 | ||
666 | CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB | 687 | CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB |
667 | 688 | ||
668 | SCHEDULER_INTERVALS_MS.badActorFollow = 10000 | 689 | SCHEDULER_INTERVALS_MS.actorFollowScores = 1000 |
669 | SCHEDULER_INTERVALS_MS.removeOldJobs = 10000 | 690 | SCHEDULER_INTERVALS_MS.removeOldJobs = 10000 |
670 | SCHEDULER_INTERVALS_MS.updateVideos = 5000 | 691 | SCHEDULER_INTERVALS_MS.updateVideos = 5000 |
671 | REPEAT_JOBS['videos-views'] = { every: 5000 } | 692 | REPEAT_JOBS['videos-views'] = { every: 5000 } |
@@ -673,6 +694,7 @@ if (isTestInstance() === true) { | |||
673 | REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1 | 694 | REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1 |
674 | 695 | ||
675 | VIDEO_VIEW_LIFETIME = 1000 // 1 second | 696 | VIDEO_VIEW_LIFETIME = 1000 // 1 second |
697 | CONTACT_FORM_LIFETIME = 1000 // 1 second | ||
676 | 698 | ||
677 | JOB_ATTEMPTS['email'] = 1 | 699 | JOB_ATTEMPTS['email'] = 1 |
678 | 700 | ||
@@ -681,13 +703,12 @@ if (isTestInstance() === true) { | |||
681 | ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' | 703 | ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' |
682 | } | 704 | } |
683 | 705 | ||
684 | updateWebserverConfig() | 706 | updateWebserverUrls() |
685 | 707 | ||
686 | // --------------------------------------------------------------------------- | 708 | // --------------------------------------------------------------------------- |
687 | 709 | ||
688 | export { | 710 | export { |
689 | API_VERSION, | 711 | API_VERSION, |
690 | VIDEO_CAPTIONS_MIMETYPE_EXT, | ||
691 | AVATARS_SIZE, | 712 | AVATARS_SIZE, |
692 | ACCEPT_HEADERS, | 713 | ACCEPT_HEADERS, |
693 | BCRYPT_SALT_SIZE, | 714 | BCRYPT_SALT_SIZE, |
@@ -715,7 +736,6 @@ export { | |||
715 | FEEDS, | 736 | FEEDS, |
716 | JOB_TTL, | 737 | JOB_TTL, |
717 | NSFW_POLICY_TYPES, | 738 | NSFW_POLICY_TYPES, |
718 | TORRENT_MIMETYPE_EXT, | ||
719 | STATIC_MAX_AGE, | 739 | STATIC_MAX_AGE, |
720 | STATIC_PATHS, | 740 | STATIC_PATHS, |
721 | VIDEO_IMPORT_TIMEOUT, | 741 | VIDEO_IMPORT_TIMEOUT, |
@@ -728,7 +748,6 @@ export { | |||
728 | VIDEO_LICENCES, | 748 | VIDEO_LICENCES, |
729 | VIDEO_STATES, | 749 | VIDEO_STATES, |
730 | VIDEO_RATE_TYPES, | 750 | VIDEO_RATE_TYPES, |
731 | VIDEO_MIMETYPE_EXT, | ||
732 | VIDEO_TRANSCODING_FPS, | 751 | VIDEO_TRANSCODING_FPS, |
733 | FFMPEG_NICE, | 752 | FFMPEG_NICE, |
734 | VIDEO_ABUSE_STATES, | 753 | VIDEO_ABUSE_STATES, |
@@ -736,18 +755,18 @@ export { | |||
736 | USER_PASSWORD_RESET_LIFETIME, | 755 | USER_PASSWORD_RESET_LIFETIME, |
737 | MEMOIZE_TTL, | 756 | MEMOIZE_TTL, |
738 | USER_EMAIL_VERIFY_LIFETIME, | 757 | USER_EMAIL_VERIFY_LIFETIME, |
739 | IMAGE_MIMETYPE_EXT, | ||
740 | OVERVIEWS, | 758 | OVERVIEWS, |
741 | SCHEDULER_INTERVALS_MS, | 759 | SCHEDULER_INTERVALS_MS, |
742 | REPEAT_JOBS, | 760 | REPEAT_JOBS, |
743 | STATIC_DOWNLOAD_PATHS, | 761 | STATIC_DOWNLOAD_PATHS, |
744 | RATES_LIMIT, | 762 | RATES_LIMIT, |
745 | VIDEO_EXT_MIMETYPE, | 763 | MIMETYPES, |
746 | CRAWL_REQUEST_CONCURRENCY, | 764 | CRAWL_REQUEST_CONCURRENCY, |
747 | JOB_COMPLETED_LIFETIME, | 765 | JOB_COMPLETED_LIFETIME, |
748 | HTTP_SIGNATURE, | 766 | HTTP_SIGNATURE, |
749 | VIDEO_IMPORT_STATES, | 767 | VIDEO_IMPORT_STATES, |
750 | VIDEO_VIEW_LIFETIME, | 768 | VIDEO_VIEW_LIFETIME, |
769 | CONTACT_FORM_LIFETIME, | ||
751 | buildLanguages | 770 | buildLanguages |
752 | } | 771 | } |
753 | 772 | ||
@@ -764,16 +783,50 @@ function getLocalConfigFilePath () { | |||
764 | return join(dirname(configSources[ 0 ].name), filename + '.json') | 783 | return join(dirname(configSources[ 0 ].name), filename + '.json') |
765 | } | 784 | } |
766 | 785 | ||
767 | function updateWebserverConfig () { | 786 | function buildVideoMimetypeExt () { |
787 | const data = { | ||
788 | 'video/webm': '.webm', | ||
789 | 'video/ogg': '.ogv', | ||
790 | 'video/mp4': '.mp4' | ||
791 | } | ||
792 | |||
793 | if (CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) { | ||
794 | Object.assign(data, { | ||
795 | 'video/quicktime': '.mov', | ||
796 | 'video/x-msvideo': '.avi', | ||
797 | 'video/x-flv': '.flv', | ||
798 | 'video/x-matroska': '.mkv', | ||
799 | 'application/octet-stream': '.mkv', | ||
800 | 'video/avi': '.avi' | ||
801 | }) | ||
802 | } | ||
803 | |||
804 | return data | ||
805 | } | ||
806 | |||
807 | function updateWebserverUrls () { | ||
768 | CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT) | 808 | CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT) |
769 | CONFIG.WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP) | 809 | CONFIG.WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP) |
770 | } | 810 | } |
771 | 811 | ||
812 | function updateWebserverConfig () { | ||
813 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname() | ||
814 | |||
815 | MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt() | ||
816 | MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) | ||
817 | } | ||
818 | |||
819 | function buildVideosExtname () { | ||
820 | return CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS | ||
821 | ? [ '.mp4', '.ogv', '.webm', '.mkv', '.mov', '.avi', '.flv' ] | ||
822 | : [ '.mp4', '.ogv', '.webm' ] | ||
823 | } | ||
824 | |||
772 | function buildVideosRedundancy (objs: any[]): VideosRedundancy[] { | 825 | function buildVideosRedundancy (objs: any[]): VideosRedundancy[] { |
773 | if (!objs) return [] | 826 | if (!objs) return [] |
774 | 827 | ||
775 | return objs.map(obj => { | 828 | return objs.map(obj => { |
776 | return Object.assign(obj, { | 829 | return Object.assign({}, obj, { |
777 | minLifetime: parseDuration(obj.min_lifetime), | 830 | minLifetime: parseDuration(obj.min_lifetime), |
778 | size: bytes.parse(obj.size), | 831 | size: bytes.parse(obj.size), |
779 | minViews: obj.min_views | 832 | minViews: obj.min_views |
@@ -850,4 +903,5 @@ export function reloadConfig () { | |||
850 | config = require('config') | 903 | config = require('config') |
851 | 904 | ||
852 | updateWebserverConfig() | 905 | updateWebserverConfig() |
906 | updateWebserverUrls() | ||
853 | } | 907 | } |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 40cd659ab..84ad2079b 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -31,6 +31,8 @@ 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' | 32 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
33 | import { ServerBlocklistModel } from '../models/server/server-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' | ||
34 | 36 | ||
35 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 37 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
36 | 38 | ||
@@ -95,7 +97,9 @@ async function initDatabaseModels (silent: boolean) { | |||
95 | VideoRedundancyModel, | 97 | VideoRedundancyModel, |
96 | UserVideoHistoryModel, | 98 | UserVideoHistoryModel, |
97 | AccountBlocklistModel, | 99 | AccountBlocklistModel, |
98 | ServerBlocklistModel | 100 | ServerBlocklistModel, |
101 | UserNotificationModel, | ||
102 | UserNotificationSettingModel | ||
99 | ]) | 103 | ]) |
100 | 104 | ||
101 | // Check extensions exist in the database | 105 | // Check extensions exist in the database |
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/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 504263c99..8215840da 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -1,11 +1,10 @@ | |||
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 { checkUrlsSameHost, getAPUrl } 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' |
@@ -13,7 +12,7 @@ import { logger } from '../../helpers/logger' | |||
13 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | 12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' |
14 | import { doRequest, downloadImage } from '../../helpers/requests' | 13 | import { doRequest, downloadImage } from '../../helpers/requests' |
15 | import { getUrlFromWebfinger } from '../../helpers/webfinger' | 14 | import { getUrlFromWebfinger } from '../../helpers/webfinger' |
16 | import { AVATARS_SIZE, 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 = getAPUrl(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) |
@@ -172,15 +171,13 @@ async function fetchActorTotalItems (url: string) { | |||
172 | 171 | ||
173 | async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { | 172 | async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { |
174 | if ( | 173 | if ( |
175 | 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 && |
176 | isActivityPubUrlValid(actorJSON.icon.url) | 175 | isActivityPubUrlValid(actorJSON.icon.url) |
177 | ) { | 176 | ) { |
178 | const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] | 177 | const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] |
179 | 178 | ||
180 | const avatarName = uuidv4() + extension | 179 | const avatarName = uuidv4() + extension |
181 | const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) | 180 | await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE) |
182 | |||
183 | await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE) | ||
184 | 181 | ||
185 | return avatarName | 182 | return avatarName |
186 | } | 183 | } |
@@ -204,6 +201,69 @@ async function addFetchOutboxJob (actor: ActorModel) { | |||
204 | return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | 201 | return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) |
205 | } | 202 | } |
206 | 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 | |||
207 | export { | 267 | export { |
208 | getOrCreateActorAndServerAndModel, | 268 | getOrCreateActorAndServerAndModel, |
209 | buildActorInstance, | 269 | buildActorInstance, |
@@ -211,6 +271,7 @@ export { | |||
211 | fetchActorTotalItems, | 271 | fetchActorTotalItems, |
212 | fetchAvatarIfExists, | 272 | fetchAvatarIfExists, |
213 | updateActorInstance, | 273 | updateActorInstance, |
274 | refreshActorIfNeeded, | ||
214 | updateActorAvatarInstance, | 275 | updateActorAvatarInstance, |
215 | addFetchOutboxJob | 276 | addFetchOutboxJob |
216 | } | 277 | } |
@@ -299,7 +360,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe | |||
299 | 360 | ||
300 | const actorJSON: ActivityPubActor = requestResult.body | 361 | const actorJSON: ActivityPubActor = requestResult.body |
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 | ||
@@ -375,59 +436,3 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu | |||
375 | 436 | ||
376 | return videoChannelCreated | 437 | return videoChannelCreated |
377 | } | 438 | } |
378 | |||
379 | async function refreshActorIfNeeded ( | ||
380 | actorArg: ActorModel, | ||
381 | fetchedType: ActorFetchByUrlType | ||
382 | ): Promise<{ actor: ActorModel, refreshed: boolean }> { | ||
383 | if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false } | ||
384 | |||
385 | // We need more attributes | ||
386 | const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url) | ||
387 | |||
388 | try { | ||
389 | const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) | ||
390 | const { result, statusCode } = await fetchRemoteActor(actorUrl) | ||
391 | |||
392 | if (statusCode === 404) { | ||
393 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
394 | actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() | ||
395 | return { actor: undefined, refreshed: false } | ||
396 | } | ||
397 | |||
398 | if (result === undefined) { | ||
399 | logger.warn('Cannot fetch remote actor in refresh actor.') | ||
400 | return { actor, refreshed: false } | ||
401 | } | ||
402 | |||
403 | return sequelizeTypescript.transaction(async t => { | ||
404 | updateInstanceWithAnother(actor, result.actor) | ||
405 | |||
406 | if (result.avatarName !== undefined) { | ||
407 | await updateActorAvatarInstance(actor, result.avatarName, t) | ||
408 | } | ||
409 | |||
410 | // Force update | ||
411 | actor.setDataValue('updatedAt', new Date()) | ||
412 | await actor.save({ transaction: t }) | ||
413 | |||
414 | if (actor.Account) { | ||
415 | actor.Account.set('name', result.name) | ||
416 | actor.Account.set('description', result.summary) | ||
417 | |||
418 | await actor.Account.save({ transaction: t }) | ||
419 | } else if (actor.VideoChannel) { | ||
420 | actor.VideoChannel.set('name', result.name) | ||
421 | actor.VideoChannel.set('description', result.summary) | ||
422 | actor.VideoChannel.set('support', result.support) | ||
423 | |||
424 | await actor.VideoChannel.save({ transaction: t }) | ||
425 | } | ||
426 | |||
427 | return { refreshed: true, actor } | ||
428 | }) | ||
429 | } catch (err) { | ||
430 | logger.warn('Cannot refresh actor.', { err }) | ||
431 | return { actor, refreshed: false } | ||
432 | } | ||
433 | } | ||
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 cd7ea01aa..5f4d793a5 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,36 +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' |
15 | import { getVideoDislikeActivityPubUrl } from '../url' | 11 | import { Notifier } from '../../notifier' |
16 | import { VideoModel } from '../../../models/video/video' | 12 | import { processViewActivity } from './process-view' |
13 | import { processDislikeActivity } from './process-dislike' | ||
14 | import { processFlagActivity } from './process-flag' | ||
17 | 15 | ||
18 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { | 16 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { |
19 | const activityObject = activity.object | 17 | const activityObject = activity.object |
20 | const activityType = activityObject.type | 18 | const activityType = activityObject.type |
21 | 19 | ||
22 | if (activityType === 'View') { | 20 | if (activityType === 'View') { |
23 | return processCreateView(byActor, activity) | 21 | return processViewActivity(activity, byActor) |
24 | } else if (activityType === 'Dislike') { | 22 | } |
25 | return retryTransactionWrapper(processCreateDislike, byActor, activity) | 23 | |
26 | } 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') { | ||
27 | return processCreateVideo(activity) | 33 | return processCreateVideo(activity) |
28 | } else if (activityType === 'Flag') { | 34 | } |
29 | return retryTransactionWrapper(processCreateVideoAbuse, byActor, activityObject as VideoAbuseObject) | 35 | |
30 | } else if (activityType === 'Note') { | 36 | if (activityType === 'Note') { |
31 | return retryTransactionWrapper(processCreateVideoComment, byActor, activity) | 37 | return retryTransactionWrapper(processCreateVideoComment, activity, byActor) |
32 | } else if (activityType === 'CacheFile') { | 38 | } |
33 | return retryTransactionWrapper(processCacheFile, byActor, activity) | 39 | |
40 | if (activityType === 'CacheFile') { | ||
41 | return retryTransactionWrapper(processCacheFile, activity, byActor) | ||
34 | } | 42 | } |
35 | 43 | ||
36 | 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 }) |
@@ -48,61 +56,14 @@ export { | |||
48 | async function processCreateVideo (activity: ActivityCreate) { | 56 | async function processCreateVideo (activity: ActivityCreate) { |
49 | const videoToCreateData = activity.object as VideoTorrentObject | 57 | const videoToCreateData = activity.object as VideoTorrentObject |
50 | 58 | ||
51 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData }) | 59 | const { video, created } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData }) |
52 | 60 | ||
53 | return video | 61 | if (created) Notifier.Instance.notifyOnNewVideo(video) |
54 | } | ||
55 | |||
56 | async function processCreateDislike (byActor: ActorModel, activity: ActivityCreate) { | ||
57 | const dislike = activity.object as DislikeObject | ||
58 | const byAccount = byActor.Account | ||
59 | |||
60 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
61 | |||
62 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) | ||
63 | 62 | ||
64 | return sequelizeTypescript.transaction(async t => { | 63 | return video |
65 | const rate = { | ||
66 | type: 'dislike' as 'dislike', | ||
67 | videoId: video.id, | ||
68 | accountId: byAccount.id | ||
69 | } | ||
70 | |||
71 | const [ , created ] = await AccountVideoRateModel.findOrCreate({ | ||
72 | where: rate, | ||
73 | defaults: Object.assign({}, rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }), | ||
74 | transaction: t | ||
75 | }) | ||
76 | if (created === true) await video.increment('dislikes', { transaction: t }) | ||
77 | |||
78 | if (video.isOwned() && created === true) { | ||
79 | // Don't resend the activity to the sender | ||
80 | const exceptions = [ byActor ] | ||
81 | |||
82 | await forwardVideoRelatedActivity(activity, t, exceptions, video) | ||
83 | } | ||
84 | }) | ||
85 | } | ||
86 | |||
87 | async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { | ||
88 | const view = activity.object as ViewObject | ||
89 | |||
90 | const options = { | ||
91 | videoObject: view.object, | ||
92 | fetchType: 'only-video' as 'only-video' | ||
93 | } | ||
94 | const { video } = await getOrCreateVideoAndAccountAndChannel(options) | ||
95 | |||
96 | await Redis.Instance.addVideoView(video.id) | ||
97 | |||
98 | if (video.isOwned()) { | ||
99 | // Don't resend the activity to the sender | ||
100 | const exceptions = [ byActor ] | ||
101 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | ||
102 | } | ||
103 | } | 64 | } |
104 | 65 | ||
105 | async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { | 66 | async function processCacheFile (activity: ActivityCreate, byActor: ActorModel) { |
106 | const cacheFile = activity.object as CacheFileObject | 67 | const cacheFile = activity.object as CacheFileObject |
107 | 68 | ||
108 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) | 69 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) |
@@ -118,29 +79,7 @@ async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) | |||
118 | } | 79 | } |
119 | } | 80 | } |
120 | 81 | ||
121 | async function processCreateVideoAbuse (byActor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) { | 82 | async function processCreateVideoComment (activity: ActivityCreate, byActor: ActorModel) { |
122 | logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) | ||
123 | |||
124 | const account = byActor.Account | ||
125 | if (!account) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
126 | |||
127 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoAbuseToCreateData.object }) | ||
128 | |||
129 | return sequelizeTypescript.transaction(async t => { | ||
130 | const videoAbuseData = { | ||
131 | reporterAccountId: account.id, | ||
132 | reason: videoAbuseToCreateData.content, | ||
133 | videoId: video.id, | ||
134 | state: VideoAbuseState.PENDING | ||
135 | } | ||
136 | |||
137 | await VideoAbuseModel.create(videoAbuseData, { transaction: t }) | ||
138 | |||
139 | logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) | ||
140 | }) | ||
141 | } | ||
142 | |||
143 | async function processCreateVideoComment (byActor: ActorModel, activity: ActivityCreate) { | ||
144 | const commentObject = activity.object as VideoCommentObject | 83 | const commentObject = activity.object as VideoCommentObject |
145 | const byAccount = byActor.Account | 84 | const byAccount = byActor.Account |
146 | 85 | ||
@@ -148,7 +87,7 @@ async function processCreateVideoComment (byActor: ActorModel, activity: Activit | |||
148 | 87 | ||
149 | const { video } = await resolveThread(commentObject.inReplyTo) | 88 | const { video } = await resolveThread(commentObject.inReplyTo) |
150 | 89 | ||
151 | const { created } = await addVideoComment(video, commentObject.id) | 90 | const { comment, created } = await addVideoComment(video, commentObject.id) |
152 | 91 | ||
153 | if (video.isOwned() && created === true) { | 92 | if (video.isOwned() && created === true) { |
154 | // Don't resend the activity to the sender | 93 | // Don't resend the activity to the sender |
@@ -156,4 +95,6 @@ async function processCreateVideoComment (byActor: ActorModel, activity: Activit | |||
156 | 95 | ||
157 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | 96 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) |
158 | } | 97 | } |
98 | |||
99 | if (created === true) Notifier.Instance.notifyOnNewComment(comment) | ||
159 | } | 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 e8e97eece..2a04167d7 100644 --- a/server/lib/activitypub/process/process-like.ts +++ b/server/lib/activitypub/process/process-like.ts | |||
@@ -6,6 +6,7 @@ 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' | 8 | import { getVideoLikeActivityPubUrl } from '../url' |
9 | import { getAPId } from '../../../helpers/activitypub' | ||
9 | 10 | ||
10 | async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { | 11 | async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { |
11 | return retryTransactionWrapper(processLikeVideo, byActor, activity) | 12 | return retryTransactionWrapper(processLikeVideo, byActor, activity) |
@@ -20,7 +21,7 @@ export { | |||
20 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
21 | 22 | ||
22 | async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { | 23 | async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { |
23 | const videoUrl = activity.object | 24 | const videoUrl = getAPId(activity.object) |
24 | 25 | ||
25 | const byAccount = byActor.Account | 26 | const byAccount = byActor.Account |
26 | 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) |
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index 438a013b6..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 | } |
@@ -72,7 +76,9 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) { | |||
72 | } | 76 | } |
73 | 77 | ||
74 | async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { | 78 | async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { |
75 | const dislike = activity.object.object as DislikeObject | 79 | const dislike = activity.object.type === 'Dislike' |
80 | ? activity.object | ||
81 | : activity.object.object as DislikeObject | ||
76 | 82 | ||
77 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) | 83 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) |
78 | 84 | ||
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 03831a00e..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 = { |
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 bcc5cac7a..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 { checkUrlsSameHost, getAPUrl } 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,7 +25,10 @@ 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 ( | 34 | async function processActivities ( |
@@ -35,12 +41,12 @@ async function processActivities ( | |||
35 | const actorsCache: { [ url: string ]: ActorModel } = {} | 41 | const actorsCache: { [ url: string ]: ActorModel } = {} |
36 | 42 | ||
37 | for (const activity of activities) { | 43 | for (const activity of activities) { |
38 | if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) { | 44 | if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].includes(activity.type) === false) { |
39 | 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) |
40 | continue | 46 | continue |
41 | } | 47 | } |
42 | 48 | ||
43 | const actorUrl = getAPUrl(activity.actor) | 49 | const actorUrl = getAPId(activity.actor) |
44 | 50 | ||
45 | // When we fetch remote data, we don't have signature | 51 | // When we fetch remote data, we don't have signature |
46 | if (options.signatureActor && actorUrl !== options.signatureActor.url) { | 52 | if (options.signatureActor && actorUrl !== options.signatureActor.url) { |
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index 5dcba778c..1767df0ae 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -11,7 +11,7 @@ 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, getAPUrl } from '../../helpers/activitypub' | 14 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
15 | 15 | ||
16 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { | 16 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { |
17 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | 17 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined |
@@ -41,7 +41,7 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) { | |||
41 | }) | 41 | }) |
42 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | 42 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') |
43 | 43 | ||
44 | const actorUrl = getAPUrl(body.actor) | 44 | const actorUrl = getAPId(body.actor) |
45 | if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { | 45 | if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { |
46 | throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) | 46 | throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) |
47 | } | 47 | } |
@@ -78,7 +78,7 @@ async function shareByServer (video: VideoModel, t: Transaction) { | |||
78 | const serverActor = await getServerActor() | 78 | const serverActor = await getServerActor() |
79 | 79 | ||
80 | const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video) | 80 | const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video) |
81 | return VideoShareModel.findOrCreate({ | 81 | const [ serverShare ] = await VideoShareModel.findOrCreate({ |
82 | defaults: { | 82 | defaults: { |
83 | actorId: serverActor.id, | 83 | actorId: serverActor.id, |
84 | videoId: video.id, | 84 | videoId: video.id, |
@@ -88,16 +88,14 @@ async function shareByServer (video: VideoModel, t: Transaction) { | |||
88 | url: serverShareUrl | 88 | url: serverShareUrl |
89 | }, | 89 | }, |
90 | transaction: t | 90 | transaction: t |
91 | }).then(([ serverShare, created ]) => { | ||
92 | if (created) return sendVideoAnnounce(serverActor, serverShare, video, t) | ||
93 | |||
94 | return undefined | ||
95 | }) | 91 | }) |
92 | |||
93 | return sendVideoAnnounce(serverActor, serverShare, video, t) | ||
96 | } | 94 | } |
97 | 95 | ||
98 | async function shareByVideoChannel (video: VideoModel, t: Transaction) { | 96 | async function shareByVideoChannel (video: VideoModel, t: Transaction) { |
99 | const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video) | 97 | const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video) |
100 | return VideoShareModel.findOrCreate({ | 98 | const [ videoChannelShare ] = await VideoShareModel.findOrCreate({ |
101 | defaults: { | 99 | defaults: { |
102 | actorId: video.VideoChannel.actorId, | 100 | actorId: video.VideoChannel.actorId, |
103 | videoId: video.id, | 101 | videoId: video.id, |
@@ -107,11 +105,9 @@ async function shareByVideoChannel (video: VideoModel, t: Transaction) { | |||
107 | url: videoChannelShareUrl | 105 | url: videoChannelShareUrl |
108 | }, | 106 | }, |
109 | transaction: t | 107 | transaction: t |
110 | }).then(([ videoChannelShare, created ]) => { | ||
111 | if (created) return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) | ||
112 | |||
113 | return undefined | ||
114 | }) | 108 | }) |
109 | |||
110 | return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) | ||
115 | } | 111 | } |
116 | 112 | ||
117 | 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/video-comments.ts b/server/lib/activitypub/video-comments.ts index 5868e7297..e87301fe7 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts | |||
@@ -70,7 +70,7 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) { | |||
70 | throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`) | 70 | throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`) |
71 | } | 71 | } |
72 | 72 | ||
73 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | 73 | const actor = await getOrCreateActorAndServerAndModel(actorUrl, 'all') |
74 | const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) | 74 | const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) |
75 | if (!entry) return { created: false } | 75 | if (!entry) return { created: false } |
76 | 76 | ||
@@ -80,6 +80,8 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) { | |||
80 | }, | 80 | }, |
81 | defaults: entry | 81 | defaults: entry |
82 | }) | 82 | }) |
83 | comment.Account = actor.Account | ||
84 | comment.Video = videoInstance | ||
83 | 85 | ||
84 | return { comment, created } | 86 | return { comment, created } |
85 | } | 87 | } |
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index 2cce67f0c..45a2b22ea 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts | |||
@@ -9,7 +9,7 @@ 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' | 11 | import { doRequest } from '../../helpers/requests' |
12 | import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' | 12 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
13 | import { ActorModel } from '../../models/activitypub/actor' | 13 | import { ActorModel } from '../../models/activitypub/actor' |
14 | import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url' | 14 | import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url' |
15 | 15 | ||
@@ -26,7 +26,7 @@ async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRa | |||
26 | }) | 26 | }) |
27 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | 27 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') |
28 | 28 | ||
29 | const actorUrl = getAPUrl(body.actor) | 29 | const actorUrl = getAPId(body.actor) |
30 | if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { | 30 | if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { |
31 | throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) | 31 | throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) |
32 | } | 32 | } |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 998f90330..e1e523499 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -1,7 +1,6 @@ | |||
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 { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' |
7 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 6 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
@@ -11,7 +10,7 @@ import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos | |||
11 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 10 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
12 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
13 | import { doRequest, downloadImage } from '../../helpers/requests' | 12 | import { doRequest, downloadImage } from '../../helpers/requests' |
14 | import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers' | 13 | import { ACTIVITY_PUB, CONFIG, MIMETYPES, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers' |
15 | import { ActorModel } from '../../models/activitypub/actor' | 14 | import { ActorModel } from '../../models/activitypub/actor' |
16 | import { TagModel } from '../../models/video/tag' | 15 | import { TagModel } from '../../models/video/tag' |
17 | import { VideoModel } from '../../models/video/video' | 16 | import { VideoModel } from '../../models/video/video' |
@@ -29,7 +28,8 @@ import { createRates } from './video-rates' | |||
29 | import { addVideoShares, shareVideoByServerAndChannel } from './share' | 28 | import { addVideoShares, shareVideoByServerAndChannel } from './share' |
30 | import { AccountModel } from '../../models/account/account' | 29 | import { AccountModel } from '../../models/account/account' |
31 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' | 30 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' |
32 | import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' | 31 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
32 | import { Notifier } from '../notifier' | ||
33 | 33 | ||
34 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 34 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { |
35 | // If the video is not private and published, we federate it | 35 | // If the video is not private and published, we federate it |
@@ -95,9 +95,8 @@ function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Fu | |||
95 | 95 | ||
96 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { | 96 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { |
97 | const thumbnailName = video.getThumbnailName() | 97 | const thumbnailName = video.getThumbnailName() |
98 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) | ||
99 | 98 | ||
100 | return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE) | 99 | return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) |
101 | } | 100 | } |
102 | 101 | ||
103 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { | 102 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { |
@@ -156,29 +155,34 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid | |||
156 | } | 155 | } |
157 | 156 | ||
158 | async function getOrCreateVideoAndAccountAndChannel (options: { | 157 | async function getOrCreateVideoAndAccountAndChannel (options: { |
159 | videoObject: VideoTorrentObject | string, | 158 | videoObject: { id: string } | string, |
160 | syncParam?: SyncParam, | 159 | syncParam?: SyncParam, |
161 | fetchType?: VideoFetchByUrlType | 160 | fetchType?: VideoFetchByUrlType, |
161 | allowRefresh?: boolean // true by default | ||
162 | }) { | 162 | }) { |
163 | // Default params | 163 | // Default params |
164 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } | 164 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } |
165 | const fetchType = options.fetchType || 'all' | 165 | const fetchType = options.fetchType || 'all' |
166 | const allowRefresh = options.allowRefresh !== false | ||
166 | 167 | ||
167 | // Get video url | 168 | // Get video url |
168 | const videoUrl = getAPUrl(options.videoObject) | 169 | const videoUrl = getAPId(options.videoObject) |
169 | 170 | ||
170 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) | 171 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) |
171 | if (videoFromDatabase) { | 172 | if (videoFromDatabase) { |
172 | const refreshOptions = { | ||
173 | video: videoFromDatabase, | ||
174 | fetchedType: fetchType, | ||
175 | syncParam | ||
176 | } | ||
177 | 173 | ||
178 | if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) | 174 | if (allowRefresh === true) { |
179 | else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } }) | 175 | const refreshOptions = { |
176 | video: videoFromDatabase, | ||
177 | fetchedType: fetchType, | ||
178 | syncParam | ||
179 | } | ||
180 | |||
181 | if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) | ||
182 | else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } }) | ||
183 | } | ||
180 | 184 | ||
181 | return { video: videoFromDatabase } | 185 | return { video: videoFromDatabase, created: false } |
182 | } | 186 | } |
183 | 187 | ||
184 | const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) | 188 | const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) |
@@ -189,7 +193,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: { | |||
189 | 193 | ||
190 | await syncVideoExternalAttributes(video, fetchedVideo, syncParam) | 194 | await syncVideoExternalAttributes(video, fetchedVideo, syncParam) |
191 | 195 | ||
192 | return { video } | 196 | return { video, created: true } |
193 | } | 197 | } |
194 | 198 | ||
195 | async function updateVideoFromAP (options: { | 199 | async function updateVideoFromAP (options: { |
@@ -200,13 +204,14 @@ async function updateVideoFromAP (options: { | |||
200 | overrideTo?: string[] | 204 | overrideTo?: string[] |
201 | }) { | 205 | }) { |
202 | logger.debug('Updating remote video "%s".', options.videoObject.uuid) | 206 | logger.debug('Updating remote video "%s".', options.videoObject.uuid) |
207 | |||
203 | let videoFieldsSave: any | 208 | let videoFieldsSave: any |
209 | const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE | ||
210 | const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED | ||
204 | 211 | ||
205 | try { | 212 | try { |
206 | await sequelizeTypescript.transaction(async t => { | 213 | await sequelizeTypescript.transaction(async t => { |
207 | const sequelizeOptions = { | 214 | const sequelizeOptions = { transaction: t } |
208 | transaction: t | ||
209 | } | ||
210 | 215 | ||
211 | videoFieldsSave = options.video.toJSON() | 216 | videoFieldsSave = options.video.toJSON() |
212 | 217 | ||
@@ -276,6 +281,11 @@ async function updateVideoFromAP (options: { | |||
276 | } | 281 | } |
277 | }) | 282 | }) |
278 | 283 | ||
284 | // Notify our users? | ||
285 | if (wasPrivateVideo || wasUnlistedVideo) { | ||
286 | Notifier.Instance.notifyOnNewVideo(options.video) | ||
287 | } | ||
288 | |||
279 | logger.info('Remote video with uuid %s updated', options.videoObject.uuid) | 289 | logger.info('Remote video with uuid %s updated', options.videoObject.uuid) |
280 | } catch (err) { | 290 | } catch (err) { |
281 | if (options.video !== undefined && videoFieldsSave !== undefined) { | 291 | if (options.video !== undefined && videoFieldsSave !== undefined) { |
@@ -358,7 +368,7 @@ export { | |||
358 | // --------------------------------------------------------------------------- | 368 | // --------------------------------------------------------------------------- |
359 | 369 | ||
360 | function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { | 370 | function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { |
361 | const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) | 371 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) |
362 | 372 | ||
363 | const urlMediaType = url.mediaType || url.mimeType | 373 | const urlMediaType = url.mediaType || url.mimeType |
364 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') | 374 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') |
@@ -486,7 +496,7 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid | |||
486 | 496 | ||
487 | const mediaType = fileUrl.mediaType || fileUrl.mimeType | 497 | const mediaType = fileUrl.mediaType || fileUrl.mimeType |
488 | const attribute = { | 498 | const attribute = { |
489 | extname: VIDEO_MIMETYPE_EXT[ mediaType ], | 499 | extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], |
490 | infoHash: parsed.infoHash, | 500 | infoHash: parsed.infoHash, |
491 | resolution: fileUrl.height, | 501 | resolution: fileUrl.height, |
492 | size: fileUrl.size, | 502 | size: fileUrl.size, |
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..f384a254e 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`, | ||
103 | text | 157 | text |
104 | } | 158 | } |
105 | 159 | ||
106 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 160 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
107 | } | 161 | } |
108 | 162 | ||
109 | async addVideoAbuseReportJob (videoId: number) { | 163 | myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) { |
110 | const video = await VideoModel.load(videoId) | 164 | const importUrl = CONFIG.WEBSERVER.URL + '/my-account/video-imports' |
111 | if (!video) throw new Error('Unknown Video id during Abuse report.') | 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, | ||
199 | text | ||
200 | } | ||
201 | |||
202 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
203 | } | ||
204 | |||
205 | addNewCommentMentionNotification (to: string[], comment: VideoCommentModel) { | ||
206 | const accountName = comment.Account.getDisplayName() | ||
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.` |
133 | 249 | ||
134 | const user = await UserModel.loadById(video.VideoChannel.Account.userId) | 250 | const emailPayload: EmailPayload = { |
251 | to, | ||
252 | subject: '[PeerTube] New user registration on ' + CONFIG.WEBSERVER.HOST, | ||
253 | text | ||
254 | } | ||
135 | 255 | ||
136 | const reasonString = reason ? ` for the following reason: ${reason}` : '' | 256 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
137 | const blockedString = `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been blacklisted${reasonString}.` | 257 | } |
258 | |||
259 | addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) { | ||
260 | const videoName = videoBlacklist.Video.name | ||
261 | const videoUrl = CONFIG.WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() | ||
262 | |||
263 | const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' | ||
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 | addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) { | ||
300 | const text = `Hi dear user,\n\n` + | ||
301 | `It seems you forgot your password 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/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 abbd89b3b..9493945ff 100644 --- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts | |||
@@ -5,6 +5,7 @@ import { doRequest } from '../../../helpers/requests' | |||
5 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 5 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
6 | import { buildGlobalHeaders, 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[] |
@@ -38,7 +39,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) { | |||
38 | .catch(() => badUrls.push(uri)) | 39 | .catch(() => badUrls.push(uri)) |
39 | }, { concurrency: BROADCAST_CONCURRENCY }) | 40 | }, { concurrency: BROADCAST_CONCURRENCY }) |
40 | 41 | ||
41 | return ActorFollowModel.updateActorFollowsScore(goodUrls, badUrls, undefined) | 42 | return ActorFollowScoreCache.Instance.updateActorFollowsScore(goodUrls, badUrls) |
42 | } | 43 | } |
43 | 44 | ||
44 | // --------------------------------------------------------------------------- | 45 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts index d36479032..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' | ||
5 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' | 4 | import { buildGlobalHeaders, 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 |
@@ -31,9 +31,9 @@ async function processActivityPubHttpUnicast (job: Bull.Job) { | |||
31 | 31 | ||
32 | try { | 32 | try { |
33 | await doRequest(options) | 33 | await doRequest(options) |
34 | ActorFollowModel.updateActorFollowsScore([ uri ], [], undefined) | 34 | ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) |
35 | } catch (err) { | 35 | } catch (err) { |
36 | ActorFollowModel.updateActorFollowsScore([], [ uri ], undefined) | 36 | ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) |
37 | 37 | ||
38 | throw err | 38 | throw err |
39 | } | 39 | } |
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts index 7752b3b40..454b975fe 100644 --- a/server/lib/job-queue/handlers/activitypub-refresher.ts +++ b/server/lib/job-queue/handlers/activitypub-refresher.ts | |||
@@ -1,29 +1,33 @@ | |||
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 { fetchVideoByUrl } from '../../../helpers/video' | 3 | import { fetchVideoByUrl } from '../../../helpers/video' |
4 | import { refreshVideoIfNeeded } from '../../activitypub' | 4 | import { refreshVideoIfNeeded, refreshActorIfNeeded } from '../../activitypub' |
5 | import { ActorModel } from '../../../models/activitypub/actor' | ||
5 | 6 | ||
6 | export type RefreshPayload = { | 7 | export type RefreshPayload = { |
7 | videoUrl: string | 8 | type: 'video' | 'actor' |
8 | type: 'video' | 9 | url: string |
9 | } | 10 | } |
10 | 11 | ||
11 | async function refreshAPObject (job: Bull.Job) { | 12 | async function refreshAPObject (job: Bull.Job) { |
12 | const payload = job.data as RefreshPayload | 13 | const payload = job.data as RefreshPayload |
13 | logger.info('Processing AP refresher in job %d.', job.id) | ||
14 | 14 | ||
15 | if (payload.type === 'video') return refreshAPVideo(payload.videoUrl) | 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) | ||
16 | } | 19 | } |
17 | 20 | ||
18 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
19 | 22 | ||
20 | export { | 23 | export { |
24 | refreshActor, | ||
21 | refreshAPObject | 25 | refreshAPObject |
22 | } | 26 | } |
23 | 27 | ||
24 | // --------------------------------------------------------------------------- | 28 | // --------------------------------------------------------------------------- |
25 | 29 | ||
26 | async function refreshAPVideo (videoUrl: string) { | 30 | async function refreshVideo (videoUrl: string) { |
27 | const fetchType = 'all' as 'all' | 31 | const fetchType = 'all' as 'all' |
28 | const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true } | 32 | const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true } |
29 | 33 | ||
@@ -38,3 +42,13 @@ async function refreshAPVideo (videoUrl: string) { | |||
38 | await refreshVideoIfNeeded(refreshOptions) | 42 | await refreshVideoIfNeeded(refreshOptions) |
39 | } | 43 | } |
40 | } | 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/video-file.ts b/server/lib/job-queue/handlers/video-file.ts index adc0a2a15..593e43cc5 100644 --- a/server/lib/job-queue/handlers/video-file.ts +++ b/server/lib/job-queue/handlers/video-file.ts | |||
@@ -8,7 +8,8 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' | |||
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript } 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, optimizeVideofile } from '../../video-transcoding' | 11 | import { 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 |
@@ -67,17 +68,17 @@ async function processVideoFile (job: Bull.Job) { | |||
67 | async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { | 68 | async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { |
68 | if (video === undefined) return undefined | 69 | if (video === undefined) return undefined |
69 | 70 | ||
70 | return sequelizeTypescript.transaction(async t => { | 71 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { |
71 | // Maybe the video changed in database, refresh it | 72 | // Maybe the video changed in database, refresh it |
72 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | 73 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) |
73 | // Video does not exist anymore | 74 | // Video does not exist anymore |
74 | if (!videoDatabase) return undefined | 75 | if (!videoDatabase) return undefined |
75 | 76 | ||
76 | let isNewVideo = false | 77 | let videoPublished = false |
77 | 78 | ||
78 | // We transcoded the video file in another format, now we can publish it | 79 | // We transcoded the video file in another format, now we can publish it |
79 | if (videoDatabase.state !== VideoState.PUBLISHED) { | 80 | if (videoDatabase.state !== VideoState.PUBLISHED) { |
80 | isNewVideo = true | 81 | videoPublished = true |
81 | 82 | ||
82 | videoDatabase.state = VideoState.PUBLISHED | 83 | videoDatabase.state = VideoState.PUBLISHED |
83 | videoDatabase.publishedAt = new Date() | 84 | videoDatabase.publishedAt = new Date() |
@@ -85,21 +86,26 @@ async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { | |||
85 | } | 86 | } |
86 | 87 | ||
87 | // If the video was not published, we consider it is a new one for other instances | 88 | // If the video was not published, we consider it is a new one for other instances |
88 | await federateVideoIfNeeded(videoDatabase, isNewVideo, t) | 89 | await federateVideoIfNeeded(videoDatabase, videoPublished, t) |
89 | 90 | ||
90 | return undefined | 91 | return { videoDatabase, videoPublished } |
91 | }) | 92 | }) |
93 | |||
94 | if (videoPublished) { | ||
95 | Notifier.Instance.notifyOnNewVideo(videoDatabase) | ||
96 | Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) | ||
97 | } | ||
92 | } | 98 | } |
93 | 99 | ||
94 | async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) { | 100 | async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: boolean) { |
95 | if (video === undefined) return undefined | 101 | if (videoArg === undefined) return undefined |
96 | 102 | ||
97 | // Outside the transaction (IO on disk) | 103 | // Outside the transaction (IO on disk) |
98 | const { videoFileResolution } = await video.getOriginalFileResolution() | 104 | const { videoFileResolution } = await videoArg.getOriginalFileResolution() |
99 | 105 | ||
100 | return sequelizeTypescript.transaction(async t => { | 106 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { |
101 | // Maybe the video changed in database, refresh it | 107 | // Maybe the video changed in database, refresh it |
102 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | 108 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t) |
103 | // Video does not exist anymore | 109 | // Video does not exist anymore |
104 | if (!videoDatabase) return undefined | 110 | if (!videoDatabase) return undefined |
105 | 111 | ||
@@ -110,8 +116,10 @@ async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boole | |||
110 | { resolutions: resolutionsEnabled } | 116 | { resolutions: resolutionsEnabled } |
111 | ) | 117 | ) |
112 | 118 | ||
119 | let videoPublished = false | ||
120 | |||
113 | if (resolutionsEnabled.length !== 0) { | 121 | if (resolutionsEnabled.length !== 0) { |
114 | const tasks: Bluebird<any>[] = [] | 122 | const tasks: Bluebird<Bull.Job<any>>[] = [] |
115 | 123 | ||
116 | for (const resolution of resolutionsEnabled) { | 124 | for (const resolution of resolutionsEnabled) { |
117 | const dataInput = { | 125 | const dataInput = { |
@@ -127,15 +135,22 @@ async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boole | |||
127 | 135 | ||
128 | logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) | 136 | logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) |
129 | } else { | 137 | } else { |
138 | videoPublished = true | ||
139 | |||
130 | // No transcoding to do, it's now published | 140 | // No transcoding to do, it's now published |
131 | video.state = VideoState.PUBLISHED | 141 | videoDatabase.state = VideoState.PUBLISHED |
132 | video = await video.save({ transaction: t }) | 142 | videoDatabase = await videoDatabase.save({ transaction: t }) |
133 | 143 | ||
134 | logger.info('No transcoding jobs created for video %s (no resolutions).', video.uuid) | 144 | logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy }) |
135 | } | 145 | } |
136 | 146 | ||
137 | return federateVideoIfNeeded(video, isNewVideo, t) | 147 | await federateVideoIfNeeded(videoDatabase, isNewVideo, t) |
148 | |||
149 | return { videoDatabase, videoPublished } | ||
138 | }) | 150 | }) |
151 | |||
152 | if (isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) | ||
153 | if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) | ||
139 | } | 154 | } |
140 | 155 | ||
141 | // --------------------------------------------------------------------------- | 156 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 4de901c0c..12004dcd7 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -7,14 +7,15 @@ import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } fro | |||
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, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' | 9 | import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' |
10 | import { doRequestAndSaveToFile, downloadImage } 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() |
@@ -138,14 +140,13 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
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 downloadImage(options.thumbnailUrl, destThumbnailPath, THUMBNAILS_SIZE) | ||
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 downloadImage(options.thumbnailUrl, destPreviewPath, PREVIEWS_SIZE) | ||
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 038ef43e2..fa1fd13b3 100644 --- a/server/lib/job-queue/handlers/video-views.ts +++ b/server/lib/job-queue/handlers/video-views.ts | |||
@@ -23,9 +23,7 @@ async function processVideosViews () { | |||
23 | for (const videoId of videoIds) { | 23 | for (const videoId of videoIds) { |
24 | try { | 24 | try { |
25 | const views = await Redis.Instance.getVideoViews(videoId, hour) | 25 | const views = await Redis.Instance.getVideoViews(videoId, hour) |
26 | if (isNaN(views)) { | 26 | if (views) { |
27 | logger.error('Cannot process videos views of video %d in hour %d: views number is NaN (%s).', videoId, hour, views) | ||
28 | } else { | ||
29 | 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) |
30 | 28 | ||
31 | try { | 29 | try { |
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 5862e178f..ba9cbe0d9 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -88,7 +88,6 @@ class JobQueue { | |||
88 | 88 | ||
89 | queue.on('error', err => { | 89 | queue.on('error', err => { |
90 | logger.error('Error in job queue %s.', handlerName, { err }) | 90 | logger.error('Error in job queue %s.', handlerName, { err }) |
91 | process.exit(-1) | ||
92 | }) | 91 | }) |
93 | 92 | ||
94 | this.queues[handlerName] = queue | 93 | this.queues[handlerName] = queue |
@@ -166,10 +165,10 @@ class JobQueue { | |||
166 | return total | 165 | return total |
167 | } | 166 | } |
168 | 167 | ||
169 | removeOldJobs () { | 168 | async removeOldJobs () { |
170 | for (const key of Object.keys(this.queues)) { | 169 | for (const key of Object.keys(this.queues)) { |
171 | const queue = this.queues[key] | 170 | const queue = this.queues[key] |
172 | queue.clean(JOB_COMPLETED_LIFETIME, 'completed') | 171 | await queue.clean(JOB_COMPLETED_LIFETIME, 'completed') |
173 | } | 172 | } |
174 | } | 173 | } |
175 | 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 8b7f33539..f643ee226 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -6,7 +6,7 @@ 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 } from '../activitypub/url' |
@@ -16,7 +16,6 @@ import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' | |||
16 | export class VideosRedundancyScheduler extends AbstractScheduler { | 16 | export class VideosRedundancyScheduler extends AbstractScheduler { |
17 | 17 | ||
18 | private static instance: AbstractScheduler | 18 | private static instance: AbstractScheduler |
19 | private executing = false | ||
20 | 19 | ||
21 | protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL | 20 | protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL |
22 | 21 | ||
@@ -24,11 +23,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
24 | super() | 23 | super() |
25 | } | 24 | } |
26 | 25 | ||
27 | async execute () { | 26 | protected async internalExecute () { |
28 | if (this.executing) return | ||
29 | |||
30 | this.executing = true | ||
31 | |||
32 | for (const obj of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { | 27 | for (const obj of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { |
33 | logger.info('Running redundancy scheduler for strategy %s.', obj.strategy) | 28 | logger.info('Running redundancy scheduler for strategy %s.', obj.strategy) |
34 | 29 | ||
@@ -57,8 +52,6 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
57 | await this.extendsLocalExpiration() | 52 | await this.extendsLocalExpiration() |
58 | 53 | ||
59 | await this.purgeRemoteExpired() | 54 | await this.purgeRemoteExpired() |
60 | |||
61 | this.executing = false | ||
62 | } | 55 | } |
63 | 56 | ||
64 | static get Instance () { | 57 | static get Instance () { |
@@ -145,13 +138,13 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
145 | 138 | ||
146 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) | 139 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) |
147 | 140 | ||
148 | const destPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(file)) | 141 | const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file)) |
149 | await rename(tmpPath, destPath) | 142 | await move(tmpPath, destPath) |
150 | 143 | ||
151 | const createdModel = await VideoRedundancyModel.create({ | 144 | const createdModel = await VideoRedundancyModel.create({ |
152 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | 145 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), |
153 | url: getVideoCacheFileActivityPubUrl(file), | 146 | url: getVideoCacheFileActivityPubUrl(file), |
154 | fileUrl: video.getVideoFileUrl(file, CONFIG.WEBSERVER.URL), | 147 | fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL), |
155 | strategy: redundancy.strategy, | 148 | strategy: redundancy.strategy, |
156 | videoFileId: file.id, | 149 | videoFileId: file.id, |
157 | actorId: serverActor.id | 150 | actorId: serverActor.id |
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 29d6d087d..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,6 +20,8 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse | |||
18 | } | 20 | } |
19 | 21 | ||
20 | const userCreated = await userToCreate.save(userOptions) | 22 | const userCreated = await userToCreate.save(userOptions) |
23 | userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t) | ||
24 | |||
21 | const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t) | 25 | const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t) |
22 | userCreated.Account = accountCreated | 26 | userCreated.Account = accountCreated |
23 | 27 | ||
@@ -88,3 +92,22 @@ export { | |||
88 | createUserAccountAndChannel, | 92 | createUserAccountAndChannel, |
89 | createLocalAccountWithoutKeys | 93 | createLocalAccountWithoutKeys |
90 | } | 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-transcoding.ts b/server/lib/video-transcoding.ts index a78de61e5..4460f46e4 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { CONFIG } from '../initializers' | 1 | import { CONFIG } from '../initializers' |
2 | import { extname, join } 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, remove, move, 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' |
@@ -30,7 +30,7 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
30 | inputVideoFile.set('extname', newExtname) | 30 | inputVideoFile.set('extname', newExtname) |
31 | 31 | ||
32 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) | 32 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) |
33 | await rename(videoTranscodedPath, videoOutputPath) | 33 | await move(videoTranscodedPath, videoOutputPath) |
34 | const stats = await stat(videoOutputPath) | 34 | const stats = await stat(videoOutputPath) |
35 | const fps = await getVideoFileFPS(videoOutputPath) | 35 | const fps = await getVideoFileFPS(videoOutputPath) |
36 | 36 | ||
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts new file mode 100644 index 000000000..8b919af0d --- /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'"] // 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 8c1df2c3e..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,6 +30,25 @@ 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 | |||
31 | function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) { | 52 | function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) { |
32 | return new Promise(resolve => { | 53 | return new Promise(resolve => { |
33 | // Already authenticated? (or tried to) | 54 | // Already authenticated? (or tried to) |
@@ -68,6 +89,7 @@ function token (req: express.Request, res: express.Response, next: express.NextF | |||
68 | 89 | ||
69 | export { | 90 | export { |
70 | authenticate, | 91 | authenticate, |
92 | authenticateSocket, | ||
71 | authenticatePromiseIfNeeded, | 93 | authenticatePromiseIfNeeded, |
72 | optionalAuthenticate, | 94 | optionalAuthenticate, |
73 | token | 95 | token |
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 46c7f0f3a..65dd00335 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -12,3 +12,4 @@ export * from './videos' | |||
12 | export * from './webfinger' | 12 | export * from './webfinger' |
13 | export * from './search' | 13 | export * from './search' |
14 | export * from './server' | 14 | export * from './server' |
15 | export * from './user-history' | ||
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts index a491dfeb3..d85afc2ff 100644 --- a/server/middlewares/validators/server.ts +++ b/server/middlewares/validators/server.ts | |||
@@ -1,9 +1,13 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { logger } from '../../helpers/logger' | 2 | import { logger } from '../../helpers/logger' |
3 | import { areValidationErrors } from './utils' | 3 | import { areValidationErrors } from './utils' |
4 | import { isHostValid } from '../../helpers/custom-validators/servers' | 4 | import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers' |
5 | import { ServerModel } from '../../models/server/server' | 5 | import { ServerModel } from '../../models/server/server' |
6 | import { body } from 'express-validator/check' | 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' | ||
7 | 11 | ||
8 | const serverGetValidator = [ | 12 | const serverGetValidator = [ |
9 | body('host').custom(isHostValid).withMessage('Should have a valid host'), | 13 | body('host').custom(isHostValid).withMessage('Should have a valid host'), |
@@ -26,8 +30,49 @@ const serverGetValidator = [ | |||
26 | } | 30 | } |
27 | ] | 31 | ] |
28 | 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 | |||
29 | // --------------------------------------------------------------------------- | 73 | // --------------------------------------------------------------------------- |
30 | 74 | ||
31 | export { | 75 | export { |
32 | serverGetValidator | 76 | serverGetValidator, |
77 | contactAdministratorValidator | ||
33 | } | 78 | } |
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 4c0577d8f..5ceda845f 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -18,6 +18,7 @@ const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOW | |||
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) | 19 | const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) |
20 | const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) | 20 | const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) |
21 | const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS) | ||
21 | 22 | ||
22 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 23 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
23 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 24 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
@@ -35,6 +36,7 @@ const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) | |||
35 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) | 36 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) |
36 | const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS) | 37 | const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS) |
37 | const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS) | 38 | const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS) |
39 | const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS) | ||
38 | 40 | ||
39 | // --------------------------------------------------------------------------- | 41 | // --------------------------------------------------------------------------- |
40 | 42 | ||
@@ -54,5 +56,6 @@ export { | |||
54 | userSubscriptionsSortValidator, | 56 | userSubscriptionsSortValidator, |
55 | videoChannelsSearchSortValidator, | 57 | videoChannelsSearchSortValidator, |
56 | accountsBlocklistSortValidator, | 58 | accountsBlocklistSortValidator, |
57 | serversBlocklistSortValidator | 59 | serversBlocklistSortValidator, |
60 | userNotificationsSortValidator | ||
58 | } | 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 ccaf2eeb6..1bb0bfb1b 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)'), |
@@ -144,6 +144,9 @@ const usersUpdateMeValidator = [ | |||
144 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 144 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
145 | body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), | 145 | body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), |
146 | body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), | 146 | body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), |
147 | body('videosHistoryEnabled') | ||
148 | .optional() | ||
149 | .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'), | ||
147 | 150 | ||
148 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 151 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
149 | logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) | 152 | logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) |
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-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/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index fa2819235..efd6ed59e 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -2,6 +2,7 @@ import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, Updated | |||
2 | import { AccountModel } from './account' | 2 | import { AccountModel } from './account' |
3 | import { getSort } from '../utils' | 3 | import { getSort } from '../utils' |
4 | import { AccountBlock } from '../../../shared/models/blocklist' | 4 | import { AccountBlock } from '../../../shared/models/blocklist' |
5 | import { Op } from 'sequelize' | ||
5 | 6 | ||
6 | enum ScopeNames { | 7 | enum ScopeNames { |
7 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' | 8 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' |
@@ -72,6 +73,36 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> { | |||
72 | }) | 73 | }) |
73 | BlockedAccount: AccountModel | 74 | BlockedAccount: AccountModel |
74 | 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 | |||
75 | static loadByAccountAndTarget (accountId: number, targetAccountId: number) { | 106 | static loadByAccountAndTarget (accountId: number, targetAccountId: number) { |
76 | const query = { | 107 | const query = { |
77 | where: { | 108 | where: { |
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 1843603f1..017a96657 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -32,6 +32,7 @@ import { | |||
32 | isUserUsernameValid, | 32 | isUserUsernameValid, |
33 | isUserVideoQuotaDailyValid, | 33 | isUserVideoQuotaDailyValid, |
34 | isUserVideoQuotaValid, | 34 | isUserVideoQuotaValid, |
35 | isUserVideosHistoryEnabledValid, | ||
35 | isUserWebTorrentEnabledValid | 36 | isUserWebTorrentEnabledValid |
36 | } from '../../helpers/custom-validators/users' | 37 | } from '../../helpers/custom-validators/users' |
37 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 38 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
@@ -43,6 +44,11 @@ import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' | |||
43 | import { values } from 'lodash' | 44 | import { values } from 'lodash' |
44 | import { NSFW_POLICY_TYPES } from '../../initializers' | 45 | import { NSFW_POLICY_TYPES } from '../../initializers' |
45 | 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' | ||
46 | 52 | ||
47 | enum ScopeNames { | 53 | enum ScopeNames { |
48 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' | 54 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' |
@@ -53,6 +59,10 @@ enum ScopeNames { | |||
53 | { | 59 | { |
54 | model: () => AccountModel, | 60 | model: () => AccountModel, |
55 | required: true | 61 | required: true |
62 | }, | ||
63 | { | ||
64 | model: () => UserNotificationSettingModel, | ||
65 | required: true | ||
56 | } | 66 | } |
57 | ] | 67 | ] |
58 | }) | 68 | }) |
@@ -63,6 +73,10 @@ enum ScopeNames { | |||
63 | model: () => AccountModel, | 73 | model: () => AccountModel, |
64 | required: true, | 74 | required: true, |
65 | include: [ () => VideoChannelModel ] | 75 | include: [ () => VideoChannelModel ] |
76 | }, | ||
77 | { | ||
78 | model: () => UserNotificationSettingModel, | ||
79 | required: true | ||
66 | } | 80 | } |
67 | ] | 81 | ] |
68 | } | 82 | } |
@@ -116,6 +130,12 @@ export class UserModel extends Model<UserModel> { | |||
116 | 130 | ||
117 | @AllowNull(false) | 131 | @AllowNull(false) |
118 | @Default(true) | 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) | ||
119 | @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) | 139 | @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) |
120 | @Column | 140 | @Column |
121 | autoPlayVideo: boolean | 141 | autoPlayVideo: boolean |
@@ -160,6 +180,19 @@ export class UserModel extends Model<UserModel> { | |||
160 | }) | 180 | }) |
161 | Account: AccountModel | 181 | Account: AccountModel |
162 | 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 | |||
163 | @HasMany(() => OAuthTokenModel, { | 196 | @HasMany(() => OAuthTokenModel, { |
164 | foreignKey: 'userId', | 197 | foreignKey: 'userId', |
165 | onDelete: 'cascade' | 198 | onDelete: 'cascade' |
@@ -242,13 +275,12 @@ export class UserModel extends Model<UserModel> { | |||
242 | }) | 275 | }) |
243 | } | 276 | } |
244 | 277 | ||
245 | static listEmailsWithRight (right: UserRight) { | 278 | static listWithRight (right: UserRight) { |
246 | const roles = Object.keys(USER_ROLE_LABELS) | 279 | const roles = Object.keys(USER_ROLE_LABELS) |
247 | .map(k => parseInt(k, 10) as UserRole) | 280 | .map(k => parseInt(k, 10) as UserRole) |
248 | .filter(role => hasUserRight(role, right)) | 281 | .filter(role => hasUserRight(role, right)) |
249 | 282 | ||
250 | const query = { | 283 | const query = { |
251 | attribute: [ 'email' ], | ||
252 | where: { | 284 | where: { |
253 | role: { | 285 | role: { |
254 | [Sequelize.Op.in]: roles | 286 | [Sequelize.Op.in]: roles |
@@ -256,9 +288,56 @@ export class UserModel extends Model<UserModel> { | |||
256 | } | 288 | } |
257 | } | 289 | } |
258 | 290 | ||
259 | return UserModel.unscoped() | 291 | return UserModel.findAll(query) |
260 | .findAll(query) | 292 | } |
261 | .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) | ||
262 | } | 341 | } |
263 | 342 | ||
264 | static loadById (id: number) { | 343 | static loadById (id: number) { |
@@ -307,6 +386,95 @@ export class UserModel extends Model<UserModel> { | |||
307 | return UserModel.findOne(query) | 386 | return UserModel.findOne(query) |
308 | } | 387 | } |
309 | 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 | |||
310 | static getOriginalVideoFileTotalFromUser (user: UserModel) { | 478 | static getOriginalVideoFileTotalFromUser (user: UserModel) { |
311 | // 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 |
312 | const query = UserModel.generateUserQuotaBaseSQL() | 480 | const query = UserModel.generateUserQuotaBaseSQL() |
@@ -363,6 +531,7 @@ export class UserModel extends Model<UserModel> { | |||
363 | emailVerified: this.emailVerified, | 531 | emailVerified: this.emailVerified, |
364 | nsfwPolicy: this.nsfwPolicy, | 532 | nsfwPolicy: this.nsfwPolicy, |
365 | webTorrentEnabled: this.webTorrentEnabled, | 533 | webTorrentEnabled: this.webTorrentEnabled, |
534 | videosHistoryEnabled: this.videosHistoryEnabled, | ||
366 | autoPlayVideo: this.autoPlayVideo, | 535 | autoPlayVideo: this.autoPlayVideo, |
367 | role: this.role, | 536 | role: this.role, |
368 | roleLabel: USER_ROLE_LABELS[ this.role ], | 537 | roleLabel: USER_ROLE_LABELS[ this.role ], |
@@ -372,6 +541,7 @@ export class UserModel extends Model<UserModel> { | |||
372 | blocked: this.blocked, | 541 | blocked: this.blocked, |
373 | blockedReason: this.blockedReason, | 542 | blockedReason: this.blockedReason, |
374 | account: this.Account.toFormattedJSON(), | 543 | account: this.Account.toFormattedJSON(), |
544 | notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined, | ||
375 | videoChannels: [], | 545 | videoChannels: [], |
376 | videoQuotaUsed: videoQuotaUsed !== undefined | 546 | videoQuotaUsed: videoQuotaUsed !== undefined |
377 | ? parseInt(videoQuotaUsed, 10) | 547 | ? parseInt(videoQuotaUsed, 10) |
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 0a6935083..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: { |
@@ -323,7 +307,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
323 | }) | 307 | }) |
324 | } | 308 | } |
325 | 309 | ||
326 | static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) { | 310 | static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) { |
327 | const query = { | 311 | const query = { |
328 | distinct: true, | 312 | distinct: true, |
329 | offset: start, | 313 | offset: start, |
@@ -351,7 +335,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
351 | as: 'ActorFollowing', | 335 | as: 'ActorFollowing', |
352 | required: true, | 336 | required: true, |
353 | where: { | 337 | where: { |
354 | id | 338 | id: actorId |
355 | } | 339 | } |
356 | } | 340 | } |
357 | ] | 341 | ] |
@@ -366,7 +350,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
366 | }) | 350 | }) |
367 | } | 351 | } |
368 | 352 | ||
369 | static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { | 353 | static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) { |
370 | const query = { | 354 | const query = { |
371 | attributes: [], | 355 | attributes: [], |
372 | distinct: true, | 356 | distinct: true, |
@@ -374,7 +358,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
374 | limit: count, | 358 | limit: count, |
375 | order: getSort(sort), | 359 | order: getSort(sort), |
376 | where: { | 360 | where: { |
377 | actorId: id | 361 | actorId: actorId |
378 | }, | 362 | }, |
379 | include: [ | 363 | include: [ |
380 | { | 364 | { |
@@ -464,6 +448,22 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
464 | } | 448 | } |
465 | } | 449 | } |
466 | 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 | |||
467 | private static async createListAcceptedFollowForApiQuery ( | 467 | private static async createListAcceptedFollowForApiQuery ( |
468 | type: 'followers' | 'following', | 468 | type: 'followers' | 'following', |
469 | actorIds: number[], | 469 | actorIds: number[], |
@@ -518,24 +518,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
518 | } | 518 | } |
519 | } | 519 | } |
520 | 520 | ||
521 | private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) { | ||
522 | const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',') | ||
523 | |||
524 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + | ||
525 | 'WHERE id IN (' + | ||
526 | 'SELECT "actorFollow"."id" FROM "actorFollow" ' + | ||
527 | 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + | ||
528 | 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' + | ||
529 | ')' | ||
530 | |||
531 | const options = t ? { | ||
532 | type: Sequelize.QueryTypes.BULKUPDATE, | ||
533 | transaction: t | ||
534 | } : undefined | ||
535 | |||
536 | return ActorFollowModel.sequelize.query(query, options) | ||
537 | } | ||
538 | |||
539 | private static listBadActorFollows () { | 521 | private static listBadActorFollows () { |
540 | const query = { | 522 | const query = { |
541 | 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/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 9de4356b4..8f2ef2d9a 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' |
@@ -124,7 +124,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
124 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 124 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
125 | logger.info('Removing duplicated video file %s.', logIdentifier) | 125 | logger.info('Removing duplicated video file %s.', logIdentifier) |
126 | 126 | ||
127 | videoFile.Video.removeFile(videoFile) | 127 | videoFile.Video.removeFile(videoFile, true) |
128 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 128 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
129 | 129 | ||
130 | return undefined | 130 | return undefined |
@@ -395,7 +395,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
395 | ] | 395 | ] |
396 | } | 396 | } |
397 | 397 | ||
398 | return VideoRedundancyModel.find(query as any) // FIXME: typings | 398 | return VideoRedundancyModel.findOne(query as any) // FIXME: typings |
399 | .then((r: any) => ({ | 399 | .then((r: any) => ({ |
400 | totalUsed: parseInt(r.totalUsed.toString(), 10), | 400 | totalUsed: parseInt(r.totalUsed.toString(), 10), |
401 | totalVideos: r.totalVideos, | 401 | totalVideos: r.totalVideos, |
@@ -415,8 +415,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
415 | expires: this.expiresOn.toISOString(), | 415 | expires: this.expiresOn.toISOString(), |
416 | url: { | 416 | url: { |
417 | type: 'Link', | 417 | type: 'Link', |
418 | mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, | 418 | mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, |
419 | mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, | 419 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, |
420 | href: this.fileUrl, | 420 | href: this.fileUrl, |
421 | height: this.VideoFile.resolution, | 421 | height: this.VideoFile.resolution, |
422 | size: this.VideoFile.size, | 422 | size: this.VideoFile.size, |
diff --git a/server/models/utils.ts b/server/models/utils.ts index 60b0906e8..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' ]) { |
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 dd6d08139..cf6278da7 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -18,7 +18,7 @@ import { ActivityTagObject } from '../../../shared/models/activitypub/objects/co | |||
18 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 18 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
19 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' | 19 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' |
20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
21 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 21 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' |
22 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' | 22 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' |
23 | import { AccountModel } from '../account/account' | 23 | import { AccountModel } from '../account/account' |
24 | import { ActorModel } from '../activitypub/actor' | 24 | import { ActorModel } from '../activitypub/actor' |
@@ -29,6 +29,9 @@ import { VideoModel } from './video' | |||
29 | import { VideoChannelModel } from './video-channel' | 29 | import { VideoChannelModel } from './video-channel' |
30 | import { getServerActor } from '../../helpers/utils' | 30 | import { getServerActor } from '../../helpers/utils' |
31 | import { UserModel } from '../account/user' | 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' | ||
32 | 35 | ||
33 | enum ScopeNames { | 36 | enum ScopeNames { |
34 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 37 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
@@ -370,9 +373,11 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
370 | id: { | 373 | id: { |
371 | [ Sequelize.Op.in ]: Sequelize.literal('(' + | 374 | [ Sequelize.Op.in ]: Sequelize.literal('(' + |
372 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + | 375 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + |
373 | 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' + | 376 | `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + |
374 | 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' + | 377 | 'UNION ' + |
375 | '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 | ') ' + | ||
376 | 'SELECT id FROM children' + | 381 | 'SELECT id FROM children' + |
377 | ')'), | 382 | ')'), |
378 | [ Sequelize.Op.ne ]: comment.id | 383 | [ Sequelize.Op.ne ]: comment.id |
@@ -448,6 +453,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
448 | } | 453 | } |
449 | } | 454 | } |
450 | 455 | ||
456 | getCommentStaticPath () { | ||
457 | return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId() | ||
458 | } | ||
459 | |||
451 | getThreadId (): number { | 460 | getThreadId (): number { |
452 | return this.originCommentId || this.id | 461 | return this.originCommentId || this.id |
453 | } | 462 | } |
@@ -456,6 +465,34 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
456 | return this.Account.isOwned() | 465 | return this.Account.isOwned() |
457 | } | 466 | } |
458 | 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 | |||
459 | toFormattedJSON () { | 496 | toFormattedJSON () { |
460 | return { | 497 | return { |
461 | 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..1f1b76c1e 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,7 +57,8 @@ 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) |
@@ -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 e3f8d525b..de0747f22 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -2,7 +2,7 @@ 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 { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
5 | import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' | 5 | import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' |
6 | import { VideoCaptionModel } from './video-caption' | 6 | import { VideoCaptionModel } from './video-caption' |
7 | import { | 7 | import { |
8 | getVideoCommentsActivityPubUrl, | 8 | getVideoCommentsActivityPubUrl, |
@@ -207,8 +207,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
207 | for (const file of video.VideoFiles) { | 207 | for (const file of video.VideoFiles) { |
208 | url.push({ | 208 | url.push({ |
209 | type: 'Link', | 209 | type: 'Link', |
210 | mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | 210 | mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, |
211 | mediaType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | 211 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, |
212 | href: video.getVideoFileUrl(file, baseUrlHttp), | 212 | href: video.getVideoFileUrl(file, baseUrlHttp), |
213 | height: file.resolution, | 213 | height: file.resolution, |
214 | size: file.size, | 214 | size: file.size, |
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.ts b/server/models/video/video.ts index 0f18d9f0c..80a6c7832 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -94,6 +94,7 @@ import { | |||
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' | 96 | import { UserModel } from '../account/user' |
97 | import { VideoImportModel } from './video-import' | ||
97 | 98 | ||
98 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 99 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
99 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 100 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -102,17 +103,45 @@ const indexes: Sequelize.DefineIndexesOptions[] = [ | |||
102 | { fields: [ 'createdAt' ] }, | 103 | { fields: [ 'createdAt' ] }, |
103 | { fields: [ 'publishedAt' ] }, | 104 | { fields: [ 'publishedAt' ] }, |
104 | { fields: [ 'duration' ] }, | 105 | { fields: [ 'duration' ] }, |
105 | { fields: [ 'category' ] }, | ||
106 | { fields: [ 'licence' ] }, | ||
107 | { fields: [ 'nsfw' ] }, | ||
108 | { fields: [ 'language' ] }, | ||
109 | { fields: [ 'waitTranscoding' ] }, | ||
110 | { fields: [ 'state' ] }, | ||
111 | { fields: [ 'remote' ] }, | ||
112 | { fields: [ 'views' ] }, | 106 | { fields: [ 'views' ] }, |
113 | { fields: [ 'likes' ] }, | ||
114 | { fields: [ 'channelId' ] }, | 107 | { fields: [ 'channelId' ] }, |
115 | { | 108 | { |
109 | fields: [ 'category' ], // We don't care videos with an unknown category | ||
110 | where: { | ||
111 | category: { | ||
112 | [Sequelize.Op.ne]: null | ||
113 | } | ||
114 | } | ||
115 | }, | ||
116 | { | ||
117 | fields: [ 'licence' ], // We don't care videos with an unknown licence | ||
118 | where: { | ||
119 | licence: { | ||
120 | [Sequelize.Op.ne]: null | ||
121 | } | ||
122 | } | ||
123 | }, | ||
124 | { | ||
125 | fields: [ 'language' ], // We don't care videos with an unknown language | ||
126 | where: { | ||
127 | language: { | ||
128 | [Sequelize.Op.ne]: null | ||
129 | } | ||
130 | } | ||
131 | }, | ||
132 | { | ||
133 | fields: [ 'nsfw' ], // Most of the videos are not NSFW | ||
134 | where: { | ||
135 | nsfw: true | ||
136 | } | ||
137 | }, | ||
138 | { | ||
139 | fields: [ 'remote' ], // Only index local videos | ||
140 | where: { | ||
141 | remote: false | ||
142 | } | ||
143 | }, | ||
144 | { | ||
116 | fields: [ 'uuid' ], | 145 | fields: [ 'uuid' ], |
117 | unique: true | 146 | unique: true |
118 | }, | 147 | }, |
@@ -140,7 +169,7 @@ type ForAPIOptions = { | |||
140 | 169 | ||
141 | type AvailableForListIDsOptions = { | 170 | type AvailableForListIDsOptions = { |
142 | serverAccountId: number | 171 | serverAccountId: number |
143 | actorId: number | 172 | followerActorId: number |
144 | includeLocalVideos: boolean | 173 | includeLocalVideos: boolean |
145 | filter?: VideoFilter | 174 | filter?: VideoFilter |
146 | categoryOneOf?: number[] | 175 | categoryOneOf?: number[] |
@@ -153,7 +182,8 @@ type AvailableForListIDsOptions = { | |||
153 | accountId?: number | 182 | accountId?: number |
154 | videoChannelId?: number | 183 | videoChannelId?: number |
155 | trendingDays?: number | 184 | trendingDays?: number |
156 | user?: UserModel | 185 | user?: UserModel, |
186 | historyOfUser?: UserModel | ||
157 | } | 187 | } |
158 | 188 | ||
159 | @Scopes({ | 189 | @Scopes({ |
@@ -315,7 +345,7 @@ type AvailableForListIDsOptions = { | |||
315 | query.include.push(videoChannelInclude) | 345 | query.include.push(videoChannelInclude) |
316 | } | 346 | } |
317 | 347 | ||
318 | if (options.actorId) { | 348 | if (options.followerActorId) { |
319 | let localVideosReq = '' | 349 | let localVideosReq = '' |
320 | if (options.includeLocalVideos === true) { | 350 | if (options.includeLocalVideos === true) { |
321 | localVideosReq = ' UNION ALL ' + | 351 | localVideosReq = ' UNION ALL ' + |
@@ -327,7 +357,7 @@ type AvailableForListIDsOptions = { | |||
327 | } | 357 | } |
328 | 358 | ||
329 | // Force actorId to be a number to avoid SQL injections | 359 | // Force actorId to be a number to avoid SQL injections |
330 | const actorIdNumber = parseInt(options.actorId.toString(), 10) | 360 | const actorIdNumber = parseInt(options.followerActorId.toString(), 10) |
331 | query.where[ 'id' ][ Sequelize.Op.and ].push({ | 361 | query.where[ 'id' ][ Sequelize.Op.and ].push({ |
332 | [ Sequelize.Op.in ]: Sequelize.literal( | 362 | [ Sequelize.Op.in ]: Sequelize.literal( |
333 | '(' + | 363 | '(' + |
@@ -416,6 +446,21 @@ type AvailableForListIDsOptions = { | |||
416 | query.subQuery = false | 446 | query.subQuery = false |
417 | } | 447 | } |
418 | 448 | ||
449 | if (options.historyOfUser) { | ||
450 | query.include.push({ | ||
451 | model: UserVideoHistoryModel, | ||
452 | required: true, | ||
453 | where: { | ||
454 | userId: options.historyOfUser.id | ||
455 | } | ||
456 | }) | ||
457 | |||
458 | // Even if the relation is n:m, we know that a user only have 0..1 video history | ||
459 | // So we won't have multiple rows for the same video | ||
460 | // Without this, we would not be able to sort on "updatedAt" column of UserVideoHistoryModel | ||
461 | query.subQuery = false | ||
462 | } | ||
463 | |||
419 | return query | 464 | return query |
420 | }, | 465 | }, |
421 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { | 466 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { |
@@ -741,6 +786,15 @@ export class VideoModel extends Model<VideoModel> { | |||
741 | }) | 786 | }) |
742 | VideoBlacklist: VideoBlacklistModel | 787 | VideoBlacklist: VideoBlacklistModel |
743 | 788 | ||
789 | @HasOne(() => VideoImportModel, { | ||
790 | foreignKey: { | ||
791 | name: 'videoId', | ||
792 | allowNull: true | ||
793 | }, | ||
794 | onDelete: 'set null' | ||
795 | }) | ||
796 | VideoImport: VideoImportModel | ||
797 | |||
744 | @HasMany(() => VideoCaptionModel, { | 798 | @HasMany(() => VideoCaptionModel, { |
745 | foreignKey: { | 799 | foreignKey: { |
746 | name: 'videoId', | 800 | name: 'videoId', |
@@ -985,9 +1039,10 @@ export class VideoModel extends Model<VideoModel> { | |||
985 | filter?: VideoFilter, | 1039 | filter?: VideoFilter, |
986 | accountId?: number, | 1040 | accountId?: number, |
987 | videoChannelId?: number, | 1041 | videoChannelId?: number, |
988 | actorId?: number | 1042 | followerActorId?: number |
989 | trendingDays?: number, | 1043 | trendingDays?: number, |
990 | user?: UserModel | 1044 | user?: UserModel, |
1045 | historyOfUser?: UserModel | ||
991 | }, countVideos = true) { | 1046 | }, countVideos = true) { |
992 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { | 1047 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { |
993 | throw new Error('Try to filter all-local but no user has not the see all videos right') | 1048 | throw new Error('Try to filter all-local but no user has not the see all videos right') |
@@ -1008,11 +1063,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1008 | 1063 | ||
1009 | const serverActor = await getServerActor() | 1064 | const serverActor = await getServerActor() |
1010 | 1065 | ||
1011 | // actorId === null has a meaning, so just check undefined | 1066 | // followerActorId === null has a meaning, so just check undefined |
1012 | const actorId = options.actorId !== undefined ? options.actorId : serverActor.id | 1067 | const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id |
1013 | 1068 | ||
1014 | const queryOptions = { | 1069 | const queryOptions = { |
1015 | actorId, | 1070 | followerActorId, |
1016 | serverAccountId: serverActor.Account.id, | 1071 | serverAccountId: serverActor.Account.id, |
1017 | nsfw: options.nsfw, | 1072 | nsfw: options.nsfw, |
1018 | categoryOneOf: options.categoryOneOf, | 1073 | categoryOneOf: options.categoryOneOf, |
@@ -1026,6 +1081,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1026 | videoChannelId: options.videoChannelId, | 1081 | videoChannelId: options.videoChannelId, |
1027 | includeLocalVideos: options.includeLocalVideos, | 1082 | includeLocalVideos: options.includeLocalVideos, |
1028 | user: options.user, | 1083 | user: options.user, |
1084 | historyOfUser: options.historyOfUser, | ||
1029 | trendingDays | 1085 | trendingDays |
1030 | } | 1086 | } |
1031 | 1087 | ||
@@ -1118,7 +1174,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1118 | 1174 | ||
1119 | const serverActor = await getServerActor() | 1175 | const serverActor = await getServerActor() |
1120 | const queryOptions = { | 1176 | const queryOptions = { |
1121 | actorId: serverActor.id, | 1177 | followerActorId: serverActor.id, |
1122 | serverAccountId: serverActor.Account.id, | 1178 | serverAccountId: serverActor.Account.id, |
1123 | includeLocalVideos: options.includeLocalVideos, | 1179 | includeLocalVideos: options.includeLocalVideos, |
1124 | nsfw: options.nsfw, | 1180 | nsfw: options.nsfw, |
@@ -1273,11 +1329,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1273 | // threshold corresponds to how many video the field should have to be returned | 1329 | // threshold corresponds to how many video the field should have to be returned |
1274 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { | 1330 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { |
1275 | const serverActor = await getServerActor() | 1331 | const serverActor = await getServerActor() |
1276 | const actorId = serverActor.id | 1332 | const followerActorId = serverActor.id |
1277 | 1333 | ||
1278 | const scopeOptions: AvailableForListIDsOptions = { | 1334 | const scopeOptions: AvailableForListIDsOptions = { |
1279 | serverAccountId: serverActor.Account.id, | 1335 | serverAccountId: serverActor.Account.id, |
1280 | actorId, | 1336 | followerActorId, |
1281 | includeLocalVideos: true | 1337 | includeLocalVideos: true |
1282 | } | 1338 | } |
1283 | 1339 | ||
@@ -1341,7 +1397,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1341 | } | 1397 | } |
1342 | 1398 | ||
1343 | const [ count, rowsId ] = await Promise.all([ | 1399 | const [ count, rowsId ] = await Promise.all([ |
1344 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), | 1400 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined), |
1345 | VideoModel.scope(idsScope).findAll(query) | 1401 | VideoModel.scope(idsScope).findAll(query) |
1346 | ]) | 1402 | ]) |
1347 | const ids = rowsId.map(r => r.id) | 1403 | const ids = rowsId.map(r => r.id) |
@@ -1481,6 +1537,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1481 | videoFile.infoHash = parsedTorrent.infoHash | 1537 | videoFile.infoHash = parsedTorrent.infoHash |
1482 | } | 1538 | } |
1483 | 1539 | ||
1540 | getWatchStaticPath () { | ||
1541 | return '/videos/watch/' + this.uuid | ||
1542 | } | ||
1543 | |||
1484 | getEmbedStaticPath () { | 1544 | getEmbedStaticPath () { |
1485 | return '/videos/embed/' + this.uuid | 1545 | return '/videos/embed/' + this.uuid |
1486 | } | 1546 | } |
@@ -1538,8 +1598,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1538 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) | 1598 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) |
1539 | } | 1599 | } |
1540 | 1600 | ||
1541 | removeFile (videoFile: VideoFileModel) { | 1601 | removeFile (videoFile: VideoFileModel, isRedundancy = false) { |
1542 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | 1602 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR |
1603 | |||
1604 | const filePath = join(baseDir, this.getVideoFilename(videoFile)) | ||
1543 | return remove(filePath) | 1605 | return remove(filePath) |
1544 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | 1606 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) |
1545 | } | 1607 | } |
@@ -1617,6 +1679,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1617 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) | 1679 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) |
1618 | } | 1680 | } |
1619 | 1681 | ||
1682 | getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | ||
1683 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile) | ||
1684 | } | ||
1685 | |||
1620 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | 1686 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1621 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) | 1687 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) |
1622 | } | 1688 | } |
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts index ea0682634..6d90d8643 100644 --- a/server/tests/api/activitypub/client.ts +++ b/server/tests/api/activitypub/client.ts | |||
@@ -8,10 +8,10 @@ import { | |||
8 | flushTests, | 8 | flushTests, |
9 | killallServers, | 9 | killallServers, |
10 | makeActivityPubGetRequest, | 10 | makeActivityPubGetRequest, |
11 | runServer, | ||
12 | ServerInfo, | 11 | ServerInfo, |
13 | setAccessTokensToServers, uploadVideo | 12 | setAccessTokensToServers, |
14 | } from '../../utils' | 13 | uploadVideo |
14 | } from '../../../../shared/utils' | ||
15 | 15 | ||
16 | const expect = chai.expect | 16 | const expect = chai.expect |
17 | 17 | ||
diff --git a/server/tests/api/activitypub/fetch.ts b/server/tests/api/activitypub/fetch.ts index a42c606c6..03609c1a9 100644 --- a/server/tests/api/activitypub/fetch.ts +++ b/server/tests/api/activitypub/fetch.ts | |||
@@ -11,12 +11,13 @@ import { | |||
11 | killallServers, | 11 | killallServers, |
12 | ServerInfo, | 12 | ServerInfo, |
13 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | setActorField, | ||
15 | setVideoField, | ||
14 | uploadVideo, | 16 | uploadVideo, |
15 | userLogin | 17 | userLogin, |
16 | } from '../../utils' | 18 | waitJobs |
19 | } from '../../../../shared/utils' | ||
17 | import * as chai from 'chai' | 20 | import * as chai from 'chai' |
18 | import { setActorField, setVideoField } from '../../utils/miscs/sql' | ||
19 | import { waitJobs } from '../../utils/server/jobs' | ||
20 | import { Video } from '../../../../shared/models/videos' | 21 | import { Video } from '../../../../shared/models/videos' |
21 | 22 | ||
22 | const expect = chai.expect | 23 | const expect = chai.expect |
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts index 610846247..ac6e755c3 100644 --- a/server/tests/api/activitypub/helpers.ts +++ b/server/tests/api/activitypub/helpers.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { expect } from 'chai' | 4 | import { expect } from 'chai' |
5 | import { buildRequestStub } from '../../utils' | 5 | import { buildRequestStub } from '../../../../shared/utils/miscs/stubs' |
6 | import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' | 6 | import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' |
7 | import { cloneDeep } from 'lodash' | 7 | import { cloneDeep } from 'lodash' |
8 | import { buildSignedActivity } from '../../../helpers/activitypub' | 8 | import { buildSignedActivity } from '../../../helpers/activitypub' |
@@ -91,7 +91,7 @@ describe('Test activity pub helpers', function () { | |||
91 | req.headers = mastodonObject.headers | 91 | req.headers = mastodonObject.headers |
92 | req.headers.signature = 'Signature ' + req.headers.signature | 92 | req.headers.signature = 'Signature ' + req.headers.signature |
93 | 93 | ||
94 | const parsed = parseHTTPSignature(req, 3600 * 365 * 3) | 94 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) |
95 | const publicKey = require('./json/mastodon/public-key.json').publicKey | 95 | const publicKey = require('./json/mastodon/public-key.json').publicKey |
96 | 96 | ||
97 | const actor = { publicKey } | 97 | const actor = { publicKey } |
@@ -110,7 +110,7 @@ describe('Test activity pub helpers', function () { | |||
110 | req.headers = mastodonObject.headers | 110 | req.headers = mastodonObject.headers |
111 | req.headers.signature = 'Signature ' + req.headers.signature | 111 | req.headers.signature = 'Signature ' + req.headers.signature |
112 | 112 | ||
113 | const parsed = parseHTTPSignature(req, 3600 * 365 * 3) | 113 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) |
114 | const publicKey = require('./json/mastodon/bad-public-key.json').publicKey | 114 | const publicKey = require('./json/mastodon/bad-public-key.json').publicKey |
115 | 115 | ||
116 | const actor = { publicKey } | 116 | const actor = { publicKey } |
@@ -150,7 +150,7 @@ describe('Test activity pub helpers', function () { | |||
150 | 150 | ||
151 | let errored = false | 151 | let errored = false |
152 | try { | 152 | try { |
153 | parseHTTPSignature(req, 3600 * 365 * 3) | 153 | parseHTTPSignature(req, 3600 * 1000 * 365 * 10) |
154 | } catch { | 154 | } catch { |
155 | errored = true | 155 | errored = true |
156 | } | 156 | } |
@@ -168,7 +168,7 @@ describe('Test activity pub helpers', function () { | |||
168 | req.headers = mastodonObject.headers | 168 | req.headers = mastodonObject.headers |
169 | req.headers.signature = 'Signature ' + req.headers.signature | 169 | req.headers.signature = 'Signature ' + req.headers.signature |
170 | 170 | ||
171 | const parsed = parseHTTPSignature(req, 3600 * 365 * 3) | 171 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) |
172 | const publicKey = require('./json/mastodon/public-key.json').publicKey | 172 | const publicKey = require('./json/mastodon/public-key.json').publicKey |
173 | 173 | ||
174 | const actor = { publicKey } | 174 | const actor = { publicKey } |
diff --git a/server/tests/api/activitypub/refresher.ts b/server/tests/api/activitypub/refresher.ts index 67e04f79e..62ad8a0b5 100644 --- a/server/tests/api/activitypub/refresher.ts +++ b/server/tests/api/activitypub/refresher.ts | |||
@@ -1,10 +1,19 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | 1 | /* tslint:disable:no-unused-expression */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { doubleFollow, getVideo, reRunServer } from '../../utils' | 4 | import { |
5 | import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo, wait } from '../../utils/index' | 5 | doubleFollow, |
6 | import { waitJobs } from '../../utils/server/jobs' | 6 | flushAndRunMultipleServers, |
7 | import { setVideoField } from '../../utils/miscs/sql' | 7 | getVideo, |
8 | killallServers, | ||
9 | reRunServer, | ||
10 | ServerInfo, | ||
11 | setAccessTokensToServers, | ||
12 | uploadVideo, | ||
13 | wait, | ||
14 | setVideoField, | ||
15 | waitJobs | ||
16 | } from '../../../../shared/utils' | ||
8 | 17 | ||
9 | describe('Test AP refresher', function () { | 18 | describe('Test AP refresher', function () { |
10 | let servers: ServerInfo[] = [] | 19 | let servers: ServerInfo[] = [] |
@@ -13,7 +22,7 @@ describe('Test AP refresher', function () { | |||
13 | let videoUUID3: string | 22 | let videoUUID3: string |
14 | 23 | ||
15 | before(async function () { | 24 | before(async function () { |
16 | this.timeout(30000) | 25 | this.timeout(60000) |
17 | 26 | ||
18 | servers = await flushAndRunMultipleServers(2) | 27 | servers = await flushAndRunMultipleServers(2) |
19 | 28 | ||
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts index 7349749f1..342ae0fa1 100644 --- a/server/tests/api/activitypub/security.ts +++ b/server/tests/api/activitypub/security.ts | |||
@@ -2,13 +2,19 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | 4 | ||
5 | import { flushAndRunMultipleServers, flushTests, killallServers, ServerInfo } from '../../utils' | 5 | import { |
6 | flushAndRunMultipleServers, | ||
7 | flushTests, | ||
8 | killallServers, | ||
9 | makeFollowRequest, | ||
10 | makePOSTAPRequest, | ||
11 | ServerInfo, | ||
12 | setActorField | ||
13 | } from '../../../../shared/utils' | ||
6 | import { HTTP_SIGNATURE } from '../../../initializers' | 14 | import { HTTP_SIGNATURE } from '../../../initializers' |
7 | import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' | 15 | import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' |
8 | import * as chai from 'chai' | 16 | import * as chai from 'chai' |
9 | import { setActorField } from '../../utils/miscs/sql' | ||
10 | import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' | 17 | import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' |
11 | import { makeFollowRequest, makePOSTAPRequest } from '../../utils/requests/activitypub' | ||
12 | 18 | ||
13 | const expect = chai.expect | 19 | const expect = chai.expect |
14 | 20 | ||
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 index c745ac975..c20453c16 100644 --- a/server/tests/api/check-params/blocklist.ts +++ b/server/tests/api/check-params/blocklist.ts | |||
@@ -13,8 +13,12 @@ import { | |||
13 | makePostBodyRequest, | 13 | makePostBodyRequest, |
14 | ServerInfo, | 14 | ServerInfo, |
15 | setAccessTokensToServers, userLogin | 15 | setAccessTokensToServers, userLogin |
16 | } from '../../utils' | 16 | } from '../../../../shared/utils' |
17 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 17 | import { |
18 | checkBadCountPagination, | ||
19 | checkBadSortPagination, | ||
20 | checkBadStartPagination | ||
21 | } from '../../../../shared/utils/requests/check-api-params' | ||
18 | 22 | ||
19 | describe('Test blocklist API validators', function () { | 23 | describe('Test blocklist API validators', function () { |
20 | let servers: ServerInfo[] | 24 | let servers: ServerInfo[] |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index d807f910b..4038ecbf0 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, |
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 877ceb0a7..77c17036a 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -1,12 +1,13 @@ | |||
1 | // Order of the tests we want to execute | ||
2 | import './accounts' | 1 | import './accounts' |
3 | import './blocklist' | 2 | import './blocklist' |
4 | import './config' | 3 | import './config' |
4 | import './contact-form' | ||
5 | import './follows' | 5 | import './follows' |
6 | import './jobs' | 6 | import './jobs' |
7 | import './redundancy' | 7 | import './redundancy' |
8 | import './search' | 8 | import './search' |
9 | import './services' | 9 | import './services' |
10 | import './user-notifications' | ||
10 | import './user-subscriptions' | 11 | import './user-subscriptions' |
11 | import './users' | 12 | import './users' |
12 | import './video-abuses' | 13 | import './video-abuses' |
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 6af7ed43b..8a9ced7c1 100644 --- a/server/tests/api/check-params/user-subscriptions.ts +++ b/server/tests/api/check-params/user-subscriptions.ts | |||
@@ -13,9 +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 { waitJobs } from '../../utils/server/jobs' | 18 | import { |
19 | checkBadCountPagination, | ||
20 | checkBadSortPagination, | ||
21 | checkBadStartPagination | ||
22 | } from '../../../../shared/utils/requests/check-api-params' | ||
23 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
19 | 24 | ||
20 | describe('Test user subscriptions API validators', function () { | 25 | describe('Test user subscriptions API validators', function () { |
21 | const path = '/api/v1/users/me/subscriptions' | 26 | const path = '/api/v1/users/me/subscriptions' |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 273be1679..a3e8e2e9c 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', |
@@ -473,11 +485,10 @@ describe('Test users API validators', function () { | |||
473 | email: 'email@example.com', | 485 | email: 'email@example.com', |
474 | emailVerified: true, | 486 | emailVerified: true, |
475 | videoQuota: 42, | 487 | videoQuota: 42, |
476 | role: UserRole.MODERATOR | 488 | role: UserRole.USER |
477 | } | 489 | } |
478 | 490 | ||
479 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields, statusCodeExpected: 204 }) | 491 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields, statusCodeExpected: 204 }) |
480 | userAccessToken = await userLogin(server, user) | ||
481 | }) | 492 | }) |
482 | }) | 493 | }) |
483 | 494 | ||
@@ -550,13 +561,13 @@ describe('Test users API validators', function () { | |||
550 | } | 561 | } |
551 | 562 | ||
552 | it('Should fail with a too small username', async function () { | 563 | it('Should fail with a too small username', async function () { |
553 | const fields = immutableAssign(baseCorrectParams, { username: 'ji' }) | 564 | const fields = immutableAssign(baseCorrectParams, { username: '' }) |
554 | 565 | ||
555 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | 566 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) |
556 | }) | 567 | }) |
557 | 568 | ||
558 | it('Should fail with a too long username', async function () { | 569 | it('Should fail with a too long username', async function () { |
559 | const fields = immutableAssign(baseCorrectParams, { username: 'my_super_username_which_is_very_long' }) | 570 | const fields = immutableAssign(baseCorrectParams, { username: 'super'.repeat(50) }) |
560 | 571 | ||
561 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | 572 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) |
562 | }) | 573 | }) |
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 e5696224d..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 | ||
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 b51f3d2cd..7bf187007 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' |
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts index 784cd8ba1..e998c8a3d 100644 --- a/server/tests/api/check-params/videos-filter.ts +++ b/server/tests/api/check-params/videos-filter.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | ServerInfo, | 11 | ServerInfo, |
12 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | userLogin | 13 | userLogin |
14 | } from '../../utils' | 14 | } from '../../../../shared/utils' |
15 | import { UserRole } from '../../../../shared/models/users' | 15 | import { UserRole } from '../../../../shared/models/users' |
16 | 16 | ||
17 | const expect = chai.expect | 17 | const expect = chai.expect |
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 699f135c7..f26b91435 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 | ||
@@ -316,10 +320,15 @@ describe('Test videos API validator', function () { | |||
316 | 320 | ||
317 | it('Should fail without an incorrect input file', async function () { | 321 | it('Should fail without an incorrect input file', async function () { |
318 | const fields = baseCorrectParams | 322 | const fields = baseCorrectParams |
319 | const attaches = { | 323 | let attaches = { |
320 | 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short_fake.webm') | 324 | 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short_fake.webm') |
321 | } | 325 | } |
322 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 326 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) |
327 | |||
328 | attaches = { | ||
329 | 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short.mkv') | ||
330 | } | ||
331 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | ||
323 | }) | 332 | }) |
324 | 333 | ||
325 | it('Should fail with an incorrect thumbnail file', async function () { | 334 | it('Should fail with an incorrect thumbnail file', async function () { |
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index a8a2f305f..9d3ce8153 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -18,15 +18,16 @@ import { | |||
18 | wait, | 18 | wait, |
19 | waitUntilLog, | 19 | waitUntilLog, |
20 | checkVideoFilesWereRemoved, removeVideo, getVideoWithToken | 20 | checkVideoFilesWereRemoved, removeVideo, getVideoWithToken |
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 |
@@ -136,7 +137,7 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st | |||
136 | if (!videoUUID) videoUUID = video1Server2UUID | 137 | if (!videoUUID) videoUUID = video1Server2UUID |
137 | 138 | ||
138 | const webseeds = [ | 139 | const webseeds = [ |
139 | 'http://localhost:9001/static/webseed/' + videoUUID, | 140 | 'http://localhost:9001/static/redundancy/' + videoUUID, |
140 | 'http://localhost:9002/static/webseed/' + videoUUID | 141 | 'http://localhost:9002/static/webseed/' + videoUUID |
141 | ] | 142 | ] |
142 | 143 | ||
@@ -148,20 +149,23 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st | |||
148 | for (const file of video.files) { | 149 | for (const file of video.files) { |
149 | checkMagnetWebseeds(file, webseeds, server) | 150 | checkMagnetWebseeds(file, webseeds, server) |
150 | 151 | ||
151 | // Only servers 1 and 2 have the video | 152 | await makeGetRequest({ |
152 | if (server.serverNumber !== 3) { | 153 | url: servers[0].url, |
153 | await makeGetRequest({ | 154 | statusCodeExpected: 200, |
154 | url: server.url, | 155 | path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`, |
155 | statusCodeExpected: 200, | 156 | contentType: null |
156 | path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, | 157 | }) |
157 | contentType: null | 158 | await makeGetRequest({ |
158 | }) | 159 | url: servers[1].url, |
159 | } | 160 | statusCodeExpected: 200, |
161 | path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, | ||
162 | contentType: null | ||
163 | }) | ||
160 | } | 164 | } |
161 | } | 165 | } |
162 | 166 | ||
163 | for (const directory of [ 'test1', 'test2' ]) { | 167 | for (const directory of [ 'test1/redundancy', 'test2/videos' ]) { |
164 | const files = await readdir(join(root(), directory, 'videos')) | 168 | const files = await readdir(join(root(), directory)) |
165 | expect(files).to.have.length.at.least(4) | 169 | expect(files).to.have.length.at.least(4) |
166 | 170 | ||
167 | for (const resolution of [ 240, 360, 480, 720 ]) { | 171 | for (const resolution of [ 240, 360, 480, 720 ]) { |
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..bebfc7398 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,17 +33,24 @@ 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 |
@@ -59,23 +70,32 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
59 | expect(data.instance.defaultNSFWPolicy).to.equal('blur') | 70 | expect(data.instance.defaultNSFWPolicy).to.equal('blur') |
60 | expect(data.instance.customizations.javascript).to.equal('alert("coucou")') | 71 | expect(data.instance.customizations.javascript).to.equal('alert("coucou")') |
61 | expect(data.instance.customizations.css).to.equal('body { background-color: red; }') | 72 | expect(data.instance.customizations.css).to.equal('body { background-color: red; }') |
73 | |||
62 | expect(data.services.twitter.username).to.equal('@Kuja') | 74 | expect(data.services.twitter.username).to.equal('@Kuja') |
63 | expect(data.services.twitter.whitelisted).to.be.true | 75 | expect(data.services.twitter.whitelisted).to.be.true |
76 | |||
64 | expect(data.cache.previews.size).to.equal(2) | 77 | expect(data.cache.previews.size).to.equal(2) |
65 | expect(data.cache.captions.size).to.equal(3) | 78 | expect(data.cache.captions.size).to.equal(3) |
79 | |||
66 | expect(data.signup.enabled).to.be.false | 80 | expect(data.signup.enabled).to.be.false |
67 | expect(data.signup.limit).to.equal(5) | 81 | expect(data.signup.limit).to.equal(5) |
68 | expect(data.signup.requiresEmailVerification).to.be.true | 82 | expect(data.signup.requiresEmailVerification).to.be.true |
83 | |||
69 | expect(data.admin.email).to.equal('superadmin1@example.com') | 84 | expect(data.admin.email).to.equal('superadmin1@example.com') |
85 | expect(data.contactForm.enabled).to.be.false | ||
86 | |||
70 | expect(data.user.videoQuota).to.equal(5242881) | 87 | expect(data.user.videoQuota).to.equal(5242881) |
71 | expect(data.user.videoQuotaDaily).to.equal(318742) | 88 | expect(data.user.videoQuotaDaily).to.equal(318742) |
89 | |||
72 | expect(data.transcoding.enabled).to.be.true | 90 | expect(data.transcoding.enabled).to.be.true |
73 | expect(data.transcoding.threads).to.equal(1) | 91 | expect(data.transcoding.threads).to.equal(1) |
92 | expect(data.transcoding.allowAdditionalExtensions).to.be.true | ||
74 | expect(data.transcoding.resolutions['240p']).to.be.false | 93 | expect(data.transcoding.resolutions['240p']).to.be.false |
75 | expect(data.transcoding.resolutions['360p']).to.be.true | 94 | expect(data.transcoding.resolutions['360p']).to.be.true |
76 | expect(data.transcoding.resolutions['480p']).to.be.true | 95 | expect(data.transcoding.resolutions['480p']).to.be.true |
77 | expect(data.transcoding.resolutions['720p']).to.be.false | 96 | expect(data.transcoding.resolutions['720p']).to.be.false |
78 | expect(data.transcoding.resolutions['1080p']).to.be.false | 97 | expect(data.transcoding.resolutions['1080p']).to.be.false |
98 | |||
79 | expect(data.import.videos.http.enabled).to.be.false | 99 | expect(data.import.videos.http.enabled).to.be.false |
80 | expect(data.import.videos.torrent.enabled).to.be.false | 100 | expect(data.import.videos.torrent.enabled).to.be.false |
81 | } | 101 | } |
@@ -93,7 +113,7 @@ describe('Test config', function () { | |||
93 | 113 | ||
94 | it('Should have a correct config on a server with registration enabled', async function () { | 114 | it('Should have a correct config on a server with registration enabled', async function () { |
95 | const res = await getConfig(server.url) | 115 | const res = await getConfig(server.url) |
96 | const data = res.body | 116 | const data: ServerConfig = res.body |
97 | 117 | ||
98 | expect(data.signup.allowed).to.be.true | 118 | expect(data.signup.allowed).to.be.true |
99 | }) | 119 | }) |
@@ -108,11 +128,23 @@ describe('Test config', function () { | |||
108 | ]) | 128 | ]) |
109 | 129 | ||
110 | const res = await getConfig(server.url) | 130 | const res = await getConfig(server.url) |
111 | const data = res.body | 131 | const data: ServerConfig = res.body |
112 | 132 | ||
113 | expect(data.signup.allowed).to.be.false | 133 | expect(data.signup.allowed).to.be.false |
114 | }) | 134 | }) |
115 | 135 | ||
136 | it('Should have the correct video allowed extensions', async function () { | ||
137 | const res = await getConfig(server.url) | ||
138 | const data: ServerConfig = res.body | ||
139 | |||
140 | expect(data.video.file.extensions).to.have.lengthOf(3) | ||
141 | expect(data.video.file.extensions).to.contain('.mp4') | ||
142 | expect(data.video.file.extensions).to.contain('.webm') | ||
143 | expect(data.video.file.extensions).to.contain('.ogv') | ||
144 | |||
145 | expect(data.contactForm.enabled).to.be.true | ||
146 | }) | ||
147 | |||
116 | it('Should get the customized configuration', async function () { | 148 | it('Should get the customized configuration', async function () { |
117 | const res = await getCustomConfig(server.url, server.accessToken) | 149 | const res = await getCustomConfig(server.url, server.accessToken) |
118 | const data = res.body as CustomConfig | 150 | const data = res.body as CustomConfig |
@@ -156,12 +188,16 @@ describe('Test config', function () { | |||
156 | admin: { | 188 | admin: { |
157 | email: 'superadmin1@example.com' | 189 | email: 'superadmin1@example.com' |
158 | }, | 190 | }, |
191 | contactForm: { | ||
192 | enabled: false | ||
193 | }, | ||
159 | user: { | 194 | user: { |
160 | videoQuota: 5242881, | 195 | videoQuota: 5242881, |
161 | videoQuotaDaily: 318742 | 196 | videoQuotaDaily: 318742 |
162 | }, | 197 | }, |
163 | transcoding: { | 198 | transcoding: { |
164 | enabled: true, | 199 | enabled: true, |
200 | allowAdditionalExtensions: true, | ||
165 | threads: 1, | 201 | threads: 1, |
166 | resolutions: { | 202 | resolutions: { |
167 | '240p': false, | 203 | '240p': false, |
@@ -190,6 +226,18 @@ describe('Test config', function () { | |||
190 | checkUpdatedConfig(data) | 226 | checkUpdatedConfig(data) |
191 | }) | 227 | }) |
192 | 228 | ||
229 | it('Should have the correct updated video allowed extensions', async function () { | ||
230 | const res = await getConfig(server.url) | ||
231 | const data: ServerConfig = res.body | ||
232 | |||
233 | expect(data.video.file.extensions).to.have.length.above(3) | ||
234 | expect(data.video.file.extensions).to.contain('.mp4') | ||
235 | expect(data.video.file.extensions).to.contain('.webm') | ||
236 | expect(data.video.file.extensions).to.contain('.ogv') | ||
237 | expect(data.video.file.extensions).to.contain('.flv') | ||
238 | expect(data.video.file.extensions).to.contain('.mkv') | ||
239 | }) | ||
240 | |||
193 | it('Should have the configuration updated after a restart', async function () { | 241 | it('Should have the configuration updated after a restart', async function () { |
194 | this.timeout(10000) | 242 | this.timeout(10000) |
195 | 243 | ||
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 index 3135fc568..8bb073c41 100644 --- a/server/tests/api/server/follow-constraints.ts +++ b/server/tests/api/server/follow-constraints.ts | |||
@@ -2,11 +2,21 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { doubleFollow, getAccountVideos, getVideo, getVideoChannelVideos, getVideoWithToken } from '../../utils' | 5 | import { |
6 | import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index' | 6 | doubleFollow, |
7 | import { unfollow } from '../../utils/server/follows' | 7 | getAccountVideos, |
8 | import { userLogin } from '../../utils/users/login' | 8 | getVideo, |
9 | import { createUser } from '../../utils/users/users' | 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' | ||
10 | 20 | ||
11 | const expect = chai.expect | 21 | const expect = chai.expect |
12 | 22 | ||
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index e80e93e7f..b0fc5d293 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 |
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts index 0421b2b40..cd7baadad 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, updateVideo, 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 | ||
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts index 6afcab1f9..1f80cc6cf 100644 --- a/server/tests/api/server/index.ts +++ b/server/tests/api/server/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import './config' | 1 | import './config' |
2 | import './contact-form' | ||
2 | import './email' | 3 | import './email' |
3 | import './follow-constraints' | 4 | import './follow-constraints' |
4 | import './follows' | 5 | import './follows' |
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 index 6d6ce8532..3b95ce945 100644 --- a/server/tests/api/server/no-client.ts +++ b/server/tests/api/server/no-client.ts | |||
@@ -4,8 +4,8 @@ import { | |||
4 | flushTests, | 4 | flushTests, |
5 | killallServers, | 5 | killallServers, |
6 | ServerInfo | 6 | ServerInfo |
7 | } from '../../utils/index' | 7 | } from '../../../../shared/utils' |
8 | import { runServer } from '../../utils/server/servers' | 8 | import { runServer } from '../../../../shared/utils/server/servers' |
9 | 9 | ||
10 | describe('Start and stop server without web client routes', function () { | 10 | describe('Start and stop server without web client routes', function () { |
11 | let server: ServerInfo | 11 | let server: ServerInfo |
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts index e2c2a293e..d4c08c346 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 |
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 index eed4b9f3e..4bca27a94 100644 --- a/server/tests/api/users/blocklist.ts +++ b/server/tests/api/users/blocklist.ts | |||
@@ -12,16 +12,16 @@ import { | |||
12 | ServerInfo, | 12 | ServerInfo, |
13 | uploadVideo, | 13 | uploadVideo, |
14 | userLogin | 14 | userLogin |
15 | } from '../../utils/index' | 15 | } from '../../../../shared/utils/index' |
16 | import { setAccessTokensToServers } from '../../utils/users/login' | 16 | import { setAccessTokensToServers } from '../../../../shared/utils/users/login' |
17 | import { getVideosListWithToken, getVideosList } from '../../utils/videos/videos' | 17 | import { getVideosListWithToken, getVideosList } from '../../../../shared/utils/videos/videos' |
18 | import { | 18 | import { |
19 | addVideoCommentReply, | 19 | addVideoCommentReply, |
20 | addVideoCommentThread, | 20 | addVideoCommentThread, |
21 | getVideoCommentThreads, | 21 | getVideoCommentThreads, |
22 | getVideoThreadComments | 22 | getVideoThreadComments |
23 | } from '../../utils/videos/video-comments' | 23 | } from '../../../../shared/utils/videos/video-comments' |
24 | import { waitJobs } from '../../utils/server/jobs' | 24 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
25 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | 25 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' |
26 | import { | 26 | import { |
27 | addAccountToAccountBlocklist, | 27 | addAccountToAccountBlocklist, |
@@ -36,7 +36,7 @@ import { | |||
36 | removeAccountFromServerBlocklist, | 36 | removeAccountFromServerBlocklist, |
37 | removeServerFromAccountBlocklist, | 37 | removeServerFromAccountBlocklist, |
38 | removeServerFromServerBlocklist | 38 | removeServerFromServerBlocklist |
39 | } from '../../utils/users/blocklist' | 39 | } from '../../../../shared/utils/users/blocklist' |
40 | 40 | ||
41 | const expect = chai.expect | 41 | const expect = chai.expect |
42 | 42 | ||
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts index ff433315d..52ba6984e 100644 --- a/server/tests/api/users/index.ts +++ b/server/tests/api/users/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import './users-verification' | ||
2 | import './user-notifications' | ||
1 | import './blocklist' | 3 | import './blocklist' |
2 | import './user-subscriptions' | 4 | import './user-subscriptions' |
3 | import './users' | 5 | import './users' |
4 | import './users-multiple-servers' | 6 | import './users-multiple-servers' |
5 | import './users-verification' | ||
diff --git a/server/tests/api/users/user-notifications.ts b/server/tests/api/users/user-notifications.ts new file mode 100644 index 000000000..5260d64cc --- /dev/null +++ b/server/tests/api/users/user-notifications.ts | |||
@@ -0,0 +1,1017 @@ | |||
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 | await uploadVideoByLocalAccount(servers) | ||
169 | |||
170 | const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) | ||
171 | expect(notification).to.be.undefined | ||
172 | |||
173 | expect(emails).to.have.lengthOf(0) | ||
174 | expect(userNotifications).to.have.lengthOf(0) | ||
175 | }) | ||
176 | |||
177 | it('Should send a new video notification if the user follows the local video publisher', async function () { | ||
178 | this.timeout(15000) | ||
179 | |||
180 | await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9001') | ||
181 | await waitJobs(servers) | ||
182 | |||
183 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
184 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
185 | }) | ||
186 | |||
187 | it('Should send a new video notification from a remote account', async function () { | ||
188 | this.timeout(50000) // Server 2 has transcoding enabled | ||
189 | |||
190 | await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9002') | ||
191 | await waitJobs(servers) | ||
192 | |||
193 | const { name, uuid } = await uploadVideoByRemoteAccount(servers) | ||
194 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
195 | }) | ||
196 | |||
197 | it('Should send a new video notification on a scheduled publication', async function () { | ||
198 | this.timeout(20000) | ||
199 | |||
200 | // In 2 seconds | ||
201 | let updateAt = new Date(new Date().getTime() + 2000) | ||
202 | |||
203 | const data = { | ||
204 | privacy: VideoPrivacy.PRIVATE, | ||
205 | scheduleUpdate: { | ||
206 | updateAt: updateAt.toISOString(), | ||
207 | privacy: VideoPrivacy.PUBLIC | ||
208 | } | ||
209 | } | ||
210 | const { name, uuid } = await uploadVideoByLocalAccount(servers, data) | ||
211 | |||
212 | await wait(6000) | ||
213 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
214 | }) | ||
215 | |||
216 | it('Should send a new video notification on a remote scheduled publication', async function () { | ||
217 | this.timeout(20000) | ||
218 | |||
219 | // In 2 seconds | ||
220 | let updateAt = new Date(new Date().getTime() + 2000) | ||
221 | |||
222 | const data = { | ||
223 | privacy: VideoPrivacy.PRIVATE, | ||
224 | scheduleUpdate: { | ||
225 | updateAt: updateAt.toISOString(), | ||
226 | privacy: VideoPrivacy.PUBLIC | ||
227 | } | ||
228 | } | ||
229 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
230 | await waitJobs(servers) | ||
231 | |||
232 | await wait(6000) | ||
233 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
234 | }) | ||
235 | |||
236 | it('Should not send a notification before the video is published', async function () { | ||
237 | this.timeout(20000) | ||
238 | |||
239 | let updateAt = new Date(new Date().getTime() + 100000) | ||
240 | |||
241 | const data = { | ||
242 | privacy: VideoPrivacy.PRIVATE, | ||
243 | scheduleUpdate: { | ||
244 | updateAt: updateAt.toISOString(), | ||
245 | privacy: VideoPrivacy.PUBLIC | ||
246 | } | ||
247 | } | ||
248 | const { name, uuid } = await uploadVideoByLocalAccount(servers, data) | ||
249 | |||
250 | await wait(6000) | ||
251 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') | ||
252 | }) | ||
253 | |||
254 | it('Should send a new video notification when a video becomes public', async function () { | ||
255 | this.timeout(10000) | ||
256 | |||
257 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
258 | const { name, uuid } = await uploadVideoByLocalAccount(servers, data) | ||
259 | |||
260 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') | ||
261 | |||
262 | await updateVideo(servers[0].url, servers[0].accessToken, uuid, { privacy: VideoPrivacy.PUBLIC }) | ||
263 | |||
264 | await wait(500) | ||
265 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
266 | }) | ||
267 | |||
268 | it('Should send a new video notification when a remote video becomes public', async function () { | ||
269 | this.timeout(20000) | ||
270 | |||
271 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
272 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
273 | |||
274 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') | ||
275 | |||
276 | await updateVideo(servers[1].url, servers[1].accessToken, uuid, { privacy: VideoPrivacy.PUBLIC }) | ||
277 | |||
278 | await waitJobs(servers) | ||
279 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
280 | }) | ||
281 | |||
282 | it('Should not send a new video notification when a video becomes unlisted', async function () { | ||
283 | this.timeout(20000) | ||
284 | |||
285 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
286 | const { name, uuid } = await uploadVideoByLocalAccount(servers, data) | ||
287 | |||
288 | await updateVideo(servers[0].url, servers[0].accessToken, uuid, { privacy: VideoPrivacy.UNLISTED }) | ||
289 | |||
290 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') | ||
291 | }) | ||
292 | |||
293 | it('Should not send a new video notification when a remote video becomes unlisted', async function () { | ||
294 | this.timeout(20000) | ||
295 | |||
296 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
297 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
298 | |||
299 | await updateVideo(servers[1].url, servers[1].accessToken, uuid, { privacy: VideoPrivacy.UNLISTED }) | ||
300 | |||
301 | await waitJobs(servers) | ||
302 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') | ||
303 | }) | ||
304 | |||
305 | it('Should send a new video notification after a video import', async function () { | ||
306 | this.timeout(30000) | ||
307 | |||
308 | const name = 'video import ' + uuidv4() | ||
309 | |||
310 | const attributes = { | ||
311 | name, | ||
312 | channelId, | ||
313 | privacy: VideoPrivacy.PUBLIC, | ||
314 | targetUrl: getYoutubeVideoUrl() | ||
315 | } | ||
316 | const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) | ||
317 | const uuid = res.body.video.uuid | ||
318 | |||
319 | await waitJobs(servers) | ||
320 | |||
321 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
322 | }) | ||
323 | }) | ||
324 | |||
325 | describe('Comment on my video notifications', function () { | ||
326 | let baseParams: CheckerBaseParams | ||
327 | |||
328 | before(() => { | ||
329 | baseParams = { | ||
330 | server: servers[0], | ||
331 | emails, | ||
332 | socketNotifications: userNotifications, | ||
333 | token: userAccessToken | ||
334 | } | ||
335 | }) | ||
336 | |||
337 | it('Should not send a new comment notification after a comment on another video', async function () { | ||
338 | this.timeout(10000) | ||
339 | |||
340 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
341 | const uuid = resVideo.body.video.uuid | ||
342 | |||
343 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
344 | const commentId = resComment.body.comment.id | ||
345 | |||
346 | await wait(500) | ||
347 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') | ||
348 | }) | ||
349 | |||
350 | it('Should not send a new comment notification if I comment my own video', async function () { | ||
351 | this.timeout(10000) | ||
352 | |||
353 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
354 | const uuid = resVideo.body.video.uuid | ||
355 | |||
356 | const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, uuid, 'comment') | ||
357 | const commentId = resComment.body.comment.id | ||
358 | |||
359 | await wait(500) | ||
360 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') | ||
361 | }) | ||
362 | |||
363 | it('Should not send a new comment notification if the account is muted', async function () { | ||
364 | this.timeout(10000) | ||
365 | |||
366 | await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') | ||
367 | |||
368 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
369 | const uuid = resVideo.body.video.uuid | ||
370 | |||
371 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
372 | const commentId = resComment.body.comment.id | ||
373 | |||
374 | await wait(500) | ||
375 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') | ||
376 | |||
377 | await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') | ||
378 | }) | ||
379 | |||
380 | it('Should send a new comment notification after a local comment on my video', async function () { | ||
381 | this.timeout(10000) | ||
382 | |||
383 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
384 | const uuid = resVideo.body.video.uuid | ||
385 | |||
386 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
387 | const commentId = resComment.body.comment.id | ||
388 | |||
389 | await wait(500) | ||
390 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'presence') | ||
391 | }) | ||
392 | |||
393 | it('Should send a new comment notification after a remote comment on my video', async function () { | ||
394 | this.timeout(10000) | ||
395 | |||
396 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
397 | const uuid = resVideo.body.video.uuid | ||
398 | |||
399 | await waitJobs(servers) | ||
400 | |||
401 | const resComment = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment') | ||
402 | const commentId = resComment.body.comment.id | ||
403 | |||
404 | await waitJobs(servers) | ||
405 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'presence') | ||
406 | }) | ||
407 | |||
408 | it('Should send a new comment notification after a local reply on my video', async function () { | ||
409 | this.timeout(10000) | ||
410 | |||
411 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
412 | const uuid = resVideo.body.video.uuid | ||
413 | |||
414 | const resThread = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
415 | const threadId = resThread.body.comment.id | ||
416 | |||
417 | const resComment = await addVideoCommentReply(servers[0].url, servers[0].accessToken, uuid, threadId, 'reply') | ||
418 | const commentId = resComment.body.comment.id | ||
419 | |||
420 | await wait(500) | ||
421 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, threadId, 'presence') | ||
422 | }) | ||
423 | |||
424 | it('Should send a new comment notification after a remote reply on my video', async function () { | ||
425 | this.timeout(10000) | ||
426 | |||
427 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
428 | const uuid = resVideo.body.video.uuid | ||
429 | await waitJobs(servers) | ||
430 | |||
431 | const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment') | ||
432 | const threadId = resThread.body.comment.id | ||
433 | |||
434 | const resComment = await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, threadId, 'reply') | ||
435 | const commentId = resComment.body.comment.id | ||
436 | |||
437 | await waitJobs(servers) | ||
438 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, threadId, 'presence') | ||
439 | }) | ||
440 | }) | ||
441 | |||
442 | describe('Mention notifications', function () { | ||
443 | let baseParams: CheckerBaseParams | ||
444 | |||
445 | before(async () => { | ||
446 | baseParams = { | ||
447 | server: servers[0], | ||
448 | emails, | ||
449 | socketNotifications: userNotifications, | ||
450 | token: userAccessToken | ||
451 | } | ||
452 | |||
453 | await updateMyUser({ | ||
454 | url: servers[0].url, | ||
455 | accessToken: servers[0].accessToken, | ||
456 | displayName: 'super root name' | ||
457 | }) | ||
458 | |||
459 | await updateMyUser({ | ||
460 | url: servers[1].url, | ||
461 | accessToken: servers[1].accessToken, | ||
462 | displayName: 'super root 2 name' | ||
463 | }) | ||
464 | }) | ||
465 | |||
466 | it('Should not send a new mention comment notification if I mention the video owner', async function () { | ||
467 | this.timeout(10000) | ||
468 | |||
469 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
470 | const uuid = resVideo.body.video.uuid | ||
471 | |||
472 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello') | ||
473 | const commentId = resComment.body.comment.id | ||
474 | |||
475 | await wait(500) | ||
476 | await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence') | ||
477 | }) | ||
478 | |||
479 | it('Should not send a new mention comment notification if I mention myself', async function () { | ||
480 | this.timeout(10000) | ||
481 | |||
482 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
483 | const uuid = resVideo.body.video.uuid | ||
484 | |||
485 | const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, uuid, '@user_1 hello') | ||
486 | const commentId = resComment.body.comment.id | ||
487 | |||
488 | await wait(500) | ||
489 | await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence') | ||
490 | }) | ||
491 | |||
492 | it('Should not send a new mention notification if the account is muted', async function () { | ||
493 | this.timeout(10000) | ||
494 | |||
495 | await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') | ||
496 | |||
497 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
498 | const uuid = resVideo.body.video.uuid | ||
499 | |||
500 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello') | ||
501 | const commentId = resComment.body.comment.id | ||
502 | |||
503 | await wait(500) | ||
504 | await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence') | ||
505 | |||
506 | await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') | ||
507 | }) | ||
508 | |||
509 | it('Should send a new mention notification after local comments', async function () { | ||
510 | this.timeout(10000) | ||
511 | |||
512 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
513 | const uuid = resVideo.body.video.uuid | ||
514 | |||
515 | const resThread = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello 1') | ||
516 | const threadId = resThread.body.comment.id | ||
517 | |||
518 | await wait(500) | ||
519 | await checkCommentMention(baseParams, uuid, threadId, threadId, 'super root name', 'presence') | ||
520 | |||
521 | const resComment = await addVideoCommentReply(servers[0].url, servers[0].accessToken, uuid, threadId, 'hello 2 @user_1') | ||
522 | const commentId = resComment.body.comment.id | ||
523 | |||
524 | await wait(500) | ||
525 | await checkCommentMention(baseParams, uuid, commentId, threadId, 'super root name', 'presence') | ||
526 | }) | ||
527 | |||
528 | it('Should send a new mention notification after remote comments', async function () { | ||
529 | this.timeout(20000) | ||
530 | |||
531 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
532 | const uuid = resVideo.body.video.uuid | ||
533 | |||
534 | await waitJobs(servers) | ||
535 | const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'hello @user_1@localhost:9001 1') | ||
536 | const threadId = resThread.body.comment.id | ||
537 | |||
538 | await waitJobs(servers) | ||
539 | await checkCommentMention(baseParams, uuid, threadId, threadId, 'super root 2 name', 'presence') | ||
540 | |||
541 | const text = '@user_1@localhost:9001 hello 2 @root@localhost:9001' | ||
542 | const resComment = await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, threadId, text) | ||
543 | const commentId = resComment.body.comment.id | ||
544 | |||
545 | await waitJobs(servers) | ||
546 | await checkCommentMention(baseParams, uuid, commentId, threadId, 'super root 2 name', 'presence') | ||
547 | }) | ||
548 | }) | ||
549 | |||
550 | describe('Video abuse for moderators notification' , function () { | ||
551 | let baseParams: CheckerBaseParams | ||
552 | |||
553 | before(() => { | ||
554 | baseParams = { | ||
555 | server: servers[0], | ||
556 | emails, | ||
557 | socketNotifications: adminNotifications, | ||
558 | token: servers[0].accessToken | ||
559 | } | ||
560 | }) | ||
561 | |||
562 | it('Should send a notification to moderators on local video abuse', async function () { | ||
563 | this.timeout(10000) | ||
564 | |||
565 | const name = 'video for abuse ' + uuidv4() | ||
566 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | ||
567 | const uuid = resVideo.body.video.uuid | ||
568 | |||
569 | await reportVideoAbuse(servers[0].url, servers[0].accessToken, uuid, 'super reason') | ||
570 | |||
571 | await waitJobs(servers) | ||
572 | await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') | ||
573 | }) | ||
574 | |||
575 | it('Should send a notification to moderators on remote video abuse', async function () { | ||
576 | this.timeout(10000) | ||
577 | |||
578 | const name = 'video for abuse ' + uuidv4() | ||
579 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | ||
580 | const uuid = resVideo.body.video.uuid | ||
581 | |||
582 | await waitJobs(servers) | ||
583 | |||
584 | await reportVideoAbuse(servers[1].url, servers[1].accessToken, uuid, 'super reason') | ||
585 | |||
586 | await waitJobs(servers) | ||
587 | await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') | ||
588 | }) | ||
589 | }) | ||
590 | |||
591 | describe('Video blacklist on my video', function () { | ||
592 | let baseParams: CheckerBaseParams | ||
593 | |||
594 | before(() => { | ||
595 | baseParams = { | ||
596 | server: servers[0], | ||
597 | emails, | ||
598 | socketNotifications: userNotifications, | ||
599 | token: userAccessToken | ||
600 | } | ||
601 | }) | ||
602 | |||
603 | it('Should send a notification to video owner on blacklist', async function () { | ||
604 | this.timeout(10000) | ||
605 | |||
606 | const name = 'video for abuse ' + uuidv4() | ||
607 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | ||
608 | const uuid = resVideo.body.video.uuid | ||
609 | |||
610 | await addVideoToBlacklist(servers[0].url, servers[0].accessToken, uuid) | ||
611 | |||
612 | await waitJobs(servers) | ||
613 | await checkNewBlacklistOnMyVideo(baseParams, uuid, name, 'blacklist') | ||
614 | }) | ||
615 | |||
616 | it('Should send a notification to video owner on unblacklist', async function () { | ||
617 | this.timeout(10000) | ||
618 | |||
619 | const name = 'video for abuse ' + uuidv4() | ||
620 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | ||
621 | const uuid = resVideo.body.video.uuid | ||
622 | |||
623 | await addVideoToBlacklist(servers[0].url, servers[0].accessToken, uuid) | ||
624 | |||
625 | await waitJobs(servers) | ||
626 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, uuid) | ||
627 | await waitJobs(servers) | ||
628 | |||
629 | await wait(500) | ||
630 | await checkNewBlacklistOnMyVideo(baseParams, uuid, name, 'unblacklist') | ||
631 | }) | ||
632 | }) | ||
633 | |||
634 | describe('My video is published', function () { | ||
635 | let baseParams: CheckerBaseParams | ||
636 | |||
637 | before(() => { | ||
638 | baseParams = { | ||
639 | server: servers[1], | ||
640 | emails, | ||
641 | socketNotifications: adminNotificationsServer2, | ||
642 | token: servers[1].accessToken | ||
643 | } | ||
644 | }) | ||
645 | |||
646 | it('Should not send a notification if transcoding is not enabled', async function () { | ||
647 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
648 | await waitJobs(servers) | ||
649 | |||
650 | await checkVideoIsPublished(baseParams, name, uuid, 'absence') | ||
651 | }) | ||
652 | |||
653 | it('Should not send a notification if the wait transcoding is false', async function () { | ||
654 | this.timeout(50000) | ||
655 | |||
656 | await uploadVideoByRemoteAccount(servers, { waitTranscoding: false }) | ||
657 | await waitJobs(servers) | ||
658 | |||
659 | const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) | ||
660 | if (notification) { | ||
661 | expect(notification.type).to.not.equal(UserNotificationType.MY_VIDEO_PUBLISHED) | ||
662 | } | ||
663 | }) | ||
664 | |||
665 | it('Should send a notification even if the video is not transcoded in other resolutions', async function () { | ||
666 | this.timeout(50000) | ||
667 | |||
668 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, { waitTranscoding: true, fixture: 'video_short_240p.mp4' }) | ||
669 | await waitJobs(servers) | ||
670 | |||
671 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') | ||
672 | }) | ||
673 | |||
674 | it('Should send a notification with a transcoded video', async function () { | ||
675 | this.timeout(50000) | ||
676 | |||
677 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, { waitTranscoding: true }) | ||
678 | await waitJobs(servers) | ||
679 | |||
680 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') | ||
681 | }) | ||
682 | |||
683 | it('Should send a notification when an imported video is transcoded', async function () { | ||
684 | this.timeout(50000) | ||
685 | |||
686 | const name = 'video import ' + uuidv4() | ||
687 | |||
688 | const attributes = { | ||
689 | name, | ||
690 | channelId, | ||
691 | privacy: VideoPrivacy.PUBLIC, | ||
692 | targetUrl: getYoutubeVideoUrl(), | ||
693 | waitTranscoding: true | ||
694 | } | ||
695 | const res = await importVideo(servers[1].url, servers[1].accessToken, attributes) | ||
696 | const uuid = res.body.video.uuid | ||
697 | |||
698 | await waitJobs(servers) | ||
699 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') | ||
700 | }) | ||
701 | |||
702 | it('Should send a notification when the scheduled update has been proceeded', async function () { | ||
703 | this.timeout(70000) | ||
704 | |||
705 | // In 2 seconds | ||
706 | let updateAt = new Date(new Date().getTime() + 2000) | ||
707 | |||
708 | const data = { | ||
709 | privacy: VideoPrivacy.PRIVATE, | ||
710 | scheduleUpdate: { | ||
711 | updateAt: updateAt.toISOString(), | ||
712 | privacy: VideoPrivacy.PUBLIC | ||
713 | } | ||
714 | } | ||
715 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
716 | |||
717 | await wait(6000) | ||
718 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') | ||
719 | }) | ||
720 | }) | ||
721 | |||
722 | describe('My video is imported', function () { | ||
723 | let baseParams: CheckerBaseParams | ||
724 | |||
725 | before(() => { | ||
726 | baseParams = { | ||
727 | server: servers[0], | ||
728 | emails, | ||
729 | socketNotifications: adminNotifications, | ||
730 | token: servers[0].accessToken | ||
731 | } | ||
732 | }) | ||
733 | |||
734 | it('Should send a notification when the video import failed', async function () { | ||
735 | this.timeout(70000) | ||
736 | |||
737 | const name = 'video import ' + uuidv4() | ||
738 | |||
739 | const attributes = { | ||
740 | name, | ||
741 | channelId, | ||
742 | privacy: VideoPrivacy.PRIVATE, | ||
743 | targetUrl: getBadVideoUrl() | ||
744 | } | ||
745 | const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) | ||
746 | const uuid = res.body.video.uuid | ||
747 | |||
748 | await waitJobs(servers) | ||
749 | await checkMyVideoImportIsFinished(baseParams, name, uuid, getBadVideoUrl(), false, 'presence') | ||
750 | }) | ||
751 | |||
752 | it('Should send a notification when the video import succeeded', async function () { | ||
753 | this.timeout(70000) | ||
754 | |||
755 | const name = 'video import ' + uuidv4() | ||
756 | |||
757 | const attributes = { | ||
758 | name, | ||
759 | channelId, | ||
760 | privacy: VideoPrivacy.PRIVATE, | ||
761 | targetUrl: getYoutubeVideoUrl() | ||
762 | } | ||
763 | const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) | ||
764 | const uuid = res.body.video.uuid | ||
765 | |||
766 | await waitJobs(servers) | ||
767 | await checkMyVideoImportIsFinished(baseParams, name, uuid, getYoutubeVideoUrl(), true, 'presence') | ||
768 | }) | ||
769 | }) | ||
770 | |||
771 | describe('New registration', function () { | ||
772 | let baseParams: CheckerBaseParams | ||
773 | |||
774 | before(() => { | ||
775 | baseParams = { | ||
776 | server: servers[0], | ||
777 | emails, | ||
778 | socketNotifications: adminNotifications, | ||
779 | token: servers[0].accessToken | ||
780 | } | ||
781 | }) | ||
782 | |||
783 | it('Should send a notification only to moderators when a user registers on the instance', async function () { | ||
784 | await registerUser(servers[0].url, 'user_45', 'password') | ||
785 | |||
786 | await waitJobs(servers) | ||
787 | |||
788 | await checkUserRegistered(baseParams, 'user_45', 'presence') | ||
789 | |||
790 | const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } } | ||
791 | await checkUserRegistered(immutableAssign(baseParams, userOverride), 'user_45', 'absence') | ||
792 | }) | ||
793 | }) | ||
794 | |||
795 | describe('New actor follow', function () { | ||
796 | let baseParams: CheckerBaseParams | ||
797 | let myChannelName = 'super channel name' | ||
798 | let myUserName = 'super user name' | ||
799 | |||
800 | before(async () => { | ||
801 | baseParams = { | ||
802 | server: servers[0], | ||
803 | emails, | ||
804 | socketNotifications: userNotifications, | ||
805 | token: userAccessToken | ||
806 | } | ||
807 | |||
808 | await updateMyUser({ | ||
809 | url: servers[0].url, | ||
810 | accessToken: servers[0].accessToken, | ||
811 | displayName: 'super root name' | ||
812 | }) | ||
813 | |||
814 | await updateMyUser({ | ||
815 | url: servers[0].url, | ||
816 | accessToken: userAccessToken, | ||
817 | displayName: myUserName | ||
818 | }) | ||
819 | |||
820 | await updateMyUser({ | ||
821 | url: servers[1].url, | ||
822 | accessToken: servers[1].accessToken, | ||
823 | displayName: 'super root 2 name' | ||
824 | }) | ||
825 | |||
826 | await updateVideoChannel(servers[0].url, userAccessToken, 'user_1_channel', { displayName: myChannelName }) | ||
827 | }) | ||
828 | |||
829 | it('Should notify when a local channel is following one of our channel', async function () { | ||
830 | this.timeout(10000) | ||
831 | |||
832 | await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001') | ||
833 | await waitJobs(servers) | ||
834 | |||
835 | await checkNewActorFollow(baseParams, 'channel', 'root', 'super root name', myChannelName, 'presence') | ||
836 | |||
837 | await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001') | ||
838 | }) | ||
839 | |||
840 | it('Should notify when a remote channel is following one of our channel', async function () { | ||
841 | this.timeout(10000) | ||
842 | |||
843 | await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001') | ||
844 | await waitJobs(servers) | ||
845 | |||
846 | await checkNewActorFollow(baseParams, 'channel', 'root', 'super root 2 name', myChannelName, 'presence') | ||
847 | |||
848 | await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001') | ||
849 | }) | ||
850 | |||
851 | it('Should notify when a local account is following one of our channel', async function () { | ||
852 | await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:9001') | ||
853 | |||
854 | await waitJobs(servers) | ||
855 | |||
856 | await checkNewActorFollow(baseParams, 'account', 'root', 'super root name', myUserName, 'presence') | ||
857 | }) | ||
858 | |||
859 | it('Should notify when a remote account is following one of our channel', async function () { | ||
860 | await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:9001') | ||
861 | |||
862 | await waitJobs(servers) | ||
863 | |||
864 | await checkNewActorFollow(baseParams, 'account', 'root', 'super root 2 name', myUserName, 'presence') | ||
865 | }) | ||
866 | }) | ||
867 | |||
868 | describe('Mark as read', function () { | ||
869 | it('Should mark as read some notifications', async function () { | ||
870 | const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3) | ||
871 | const ids = res.body.data.map(n => n.id) | ||
872 | |||
873 | await markAsReadNotifications(servers[ 0 ].url, userAccessToken, ids) | ||
874 | }) | ||
875 | |||
876 | it('Should have the notifications marked as read', async function () { | ||
877 | const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10) | ||
878 | |||
879 | const notifications = res.body.data as UserNotification[] | ||
880 | expect(notifications[ 0 ].read).to.be.false | ||
881 | expect(notifications[ 1 ].read).to.be.false | ||
882 | expect(notifications[ 2 ].read).to.be.true | ||
883 | expect(notifications[ 3 ].read).to.be.true | ||
884 | expect(notifications[ 4 ].read).to.be.true | ||
885 | expect(notifications[ 5 ].read).to.be.false | ||
886 | }) | ||
887 | |||
888 | it('Should only list read notifications', async function () { | ||
889 | const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, false) | ||
890 | |||
891 | const notifications = res.body.data as UserNotification[] | ||
892 | for (const notification of notifications) { | ||
893 | expect(notification.read).to.be.true | ||
894 | } | ||
895 | }) | ||
896 | |||
897 | it('Should only list unread notifications', async function () { | ||
898 | const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, true) | ||
899 | |||
900 | const notifications = res.body.data as UserNotification[] | ||
901 | for (const notification of notifications) { | ||
902 | expect(notification.read).to.be.false | ||
903 | } | ||
904 | }) | ||
905 | |||
906 | it('Should mark as read all notifications', async function () { | ||
907 | await markAsReadAllNotifications(servers[ 0 ].url, userAccessToken) | ||
908 | |||
909 | const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, true) | ||
910 | |||
911 | expect(res.body.total).to.equal(0) | ||
912 | expect(res.body.data).to.have.lengthOf(0) | ||
913 | }) | ||
914 | }) | ||
915 | |||
916 | describe('Notification settings', function () { | ||
917 | let baseParams: CheckerBaseParams | ||
918 | |||
919 | before(() => { | ||
920 | baseParams = { | ||
921 | server: servers[0], | ||
922 | emails, | ||
923 | socketNotifications: userNotifications, | ||
924 | token: userAccessToken | ||
925 | } | ||
926 | }) | ||
927 | |||
928 | it('Should not have notifications', async function () { | ||
929 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | ||
930 | newVideoFromSubscription: UserNotificationSettingValue.NONE | ||
931 | })) | ||
932 | |||
933 | { | ||
934 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
935 | const info = res.body as User | ||
936 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.NONE) | ||
937 | } | ||
938 | |||
939 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
940 | |||
941 | const check = { web: true, mail: true } | ||
942 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence') | ||
943 | }) | ||
944 | |||
945 | it('Should only have web notifications', async function () { | ||
946 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | ||
947 | newVideoFromSubscription: UserNotificationSettingValue.WEB | ||
948 | })) | ||
949 | |||
950 | { | ||
951 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
952 | const info = res.body as User | ||
953 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB) | ||
954 | } | ||
955 | |||
956 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
957 | |||
958 | { | ||
959 | const check = { mail: true, web: false } | ||
960 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence') | ||
961 | } | ||
962 | |||
963 | { | ||
964 | const check = { mail: false, web: true } | ||
965 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'presence') | ||
966 | } | ||
967 | }) | ||
968 | |||
969 | it('Should only have mail notifications', async function () { | ||
970 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | ||
971 | newVideoFromSubscription: UserNotificationSettingValue.EMAIL | ||
972 | })) | ||
973 | |||
974 | { | ||
975 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
976 | const info = res.body as User | ||
977 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.EMAIL) | ||
978 | } | ||
979 | |||
980 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
981 | |||
982 | { | ||
983 | const check = { mail: false, web: true } | ||
984 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence') | ||
985 | } | ||
986 | |||
987 | { | ||
988 | const check = { mail: true, web: false } | ||
989 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'presence') | ||
990 | } | ||
991 | }) | ||
992 | |||
993 | it('Should have email and web notifications', async function () { | ||
994 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | ||
995 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | ||
996 | })) | ||
997 | |||
998 | { | ||
999 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
1000 | const info = res.body as User | ||
1001 | expect(info.notificationSettings.newVideoFromSubscription).to.equal( | ||
1002 | UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | ||
1003 | ) | ||
1004 | } | ||
1005 | |||
1006 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | ||
1007 | |||
1008 | await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') | ||
1009 | }) | ||
1010 | }) | ||
1011 | |||
1012 | after(async function () { | ||
1013 | MockSmtpServer.Instance.kill() | ||
1014 | |||
1015 | killallServers(servers) | ||
1016 | }) | ||
1017 | }) | ||
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 e7bb845b9..ad98ab1c7 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 | ||
@@ -501,10 +501,6 @@ describe('Test users', function () { | |||
501 | accessTokenUser = await userLogin(server, user) | 501 | accessTokenUser = await userLogin(server, user) |
502 | }) | 502 | }) |
503 | 503 | ||
504 | it('Should not be able to delete a user by a moderator', async function () { | ||
505 | await removeUser(server.url, 2, accessTokenUser, 403) | ||
506 | }) | ||
507 | |||
508 | it('Should be able to list video blacklist by a moderator', async function () { | 504 | it('Should be able to list video blacklist by a moderator', async function () { |
509 | await getBlacklistedVideosList(server.url, accessTokenUser) | 505 | await getBlacklistedVideosList(server.url, accessTokenUser) |
510 | }) | 506 | }) |
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 9bdb78491..97f467aae 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -3,7 +3,6 @@ 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' |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index b9ace2885..6c281e49e 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 | ||
@@ -1008,6 +1009,14 @@ describe('Test multiple servers', function () { | |||
1008 | }) | 1009 | }) |
1009 | }) | 1010 | }) |
1010 | 1011 | ||
1012 | describe('TMP directory', function () { | ||
1013 | it('Should have an empty tmp directory', async function () { | ||
1014 | for (const server of servers) { | ||
1015 | await checkTmpIsEmpty(server) | ||
1016 | } | ||
1017 | }) | ||
1018 | }) | ||
1019 | |||
1011 | after(async function () { | 1020 | after(async function () { |
1012 | killallServers(servers) | 1021 | killallServers(servers) |
1013 | 1022 | ||
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 089c3df25..069dec67c 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 | ||
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 fab577b30..000000000 --- a/server/tests/api/videos/video-blacklist-management.ts +++ /dev/null | |||
@@ -1,192 +0,0 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import { orderBy } 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 | |||
26 | describe('Test video blacklist management', function () { | ||
27 | let servers: ServerInfo[] = [] | ||
28 | let videoId: number | ||
29 | |||
30 | async function blacklistVideosOnServer (server: ServerInfo) { | ||
31 | const res = await getVideosList(server.url) | ||
32 | |||
33 | const videos = res.body.data | ||
34 | for (let video of videos) { | ||
35 | await addVideoToBlacklist(server.url, server.accessToken, video.id, 'super reason') | ||
36 | } | ||
37 | } | ||
38 | |||
39 | before(async function () { | ||
40 | this.timeout(50000) | ||
41 | |||
42 | // Run servers | ||
43 | servers = await flushAndRunMultipleServers(2) | ||
44 | |||
45 | // Get the access tokens | ||
46 | await setAccessTokensToServers(servers) | ||
47 | |||
48 | // Server 1 and server 2 follow each other | ||
49 | await doubleFollow(servers[0], servers[1]) | ||
50 | |||
51 | // Upload 2 videos on server 2 | ||
52 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 1st video', description: 'A video on server 2' }) | ||
53 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 2nd video', description: 'A video on server 2' }) | ||
54 | |||
55 | // Wait videos propagation, server 2 has transcoding enabled | ||
56 | await waitJobs(servers) | ||
57 | |||
58 | // Blacklist the two videos on server 1 | ||
59 | await blacklistVideosOnServer(servers[0]) | ||
60 | }) | ||
61 | |||
62 | describe('When listing blacklisted videos', function () { | ||
63 | it('Should display all the blacklisted videos', async function () { | ||
64 | const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken) | ||
65 | |||
66 | expect(res.body.total).to.equal(2) | ||
67 | |||
68 | const blacklistedVideos = res.body.data | ||
69 | expect(blacklistedVideos).to.be.an('array') | ||
70 | expect(blacklistedVideos.length).to.equal(2) | ||
71 | |||
72 | for (const blacklistedVideo of blacklistedVideos) { | ||
73 | expect(blacklistedVideo.reason).to.equal('super reason') | ||
74 | videoId = blacklistedVideo.video.id | ||
75 | } | ||
76 | }) | ||
77 | |||
78 | it('Should get the correct sort when sorting by descending id', async function () { | ||
79 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id') | ||
80 | expect(res.body.total).to.equal(2) | ||
81 | |||
82 | const blacklistedVideos = res.body.data | ||
83 | expect(blacklistedVideos).to.be.an('array') | ||
84 | expect(blacklistedVideos.length).to.equal(2) | ||
85 | |||
86 | const result = orderBy(res.body.data, [ 'id' ], [ 'desc' ]) | ||
87 | |||
88 | expect(blacklistedVideos).to.deep.equal(result) | ||
89 | }) | ||
90 | |||
91 | it('Should get the correct sort when sorting by descending video name', async function () { | ||
92 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
93 | expect(res.body.total).to.equal(2) | ||
94 | |||
95 | const blacklistedVideos = res.body.data | ||
96 | expect(blacklistedVideos).to.be.an('array') | ||
97 | expect(blacklistedVideos.length).to.equal(2) | ||
98 | |||
99 | const result = orderBy(res.body.data, [ 'name' ], [ 'desc' ]) | ||
100 | |||
101 | expect(blacklistedVideos).to.deep.equal(result) | ||
102 | }) | ||
103 | |||
104 | it('Should get the correct sort when sorting by ascending creation date', async function () { | ||
105 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt') | ||
106 | expect(res.body.total).to.equal(2) | ||
107 | |||
108 | const blacklistedVideos = res.body.data | ||
109 | expect(blacklistedVideos).to.be.an('array') | ||
110 | expect(blacklistedVideos.length).to.equal(2) | ||
111 | |||
112 | const result = orderBy(res.body.data, [ 'createdAt' ]) | ||
113 | |||
114 | expect(blacklistedVideos).to.deep.equal(result) | ||
115 | }) | ||
116 | }) | ||
117 | |||
118 | describe('When updating blacklisted videos', function () { | ||
119 | it('Should change the reason', async function () { | ||
120 | await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated') | ||
121 | |||
122 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
123 | const video = res.body.data.find(b => b.video.id === videoId) | ||
124 | |||
125 | expect(video.reason).to.equal('my super reason updated') | ||
126 | }) | ||
127 | }) | ||
128 | |||
129 | describe('When listing my videos', function () { | ||
130 | it('Should display blacklisted videos', async function () { | ||
131 | await blacklistVideosOnServer(servers[1]) | ||
132 | |||
133 | const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 5) | ||
134 | |||
135 | expect(res.body.total).to.equal(2) | ||
136 | expect(res.body.data).to.have.lengthOf(2) | ||
137 | |||
138 | for (const video of res.body.data) { | ||
139 | expect(video.blacklisted).to.be.true | ||
140 | expect(video.blacklistedReason).to.equal('super reason') | ||
141 | } | ||
142 | }) | ||
143 | }) | ||
144 | |||
145 | describe('When removing a blacklisted video', function () { | ||
146 | let videoToRemove: VideoAbuse | ||
147 | let blacklist = [] | ||
148 | |||
149 | it('Should not have any video in videos list on server 1', async function () { | ||
150 | const res = await getVideosList(servers[0].url) | ||
151 | expect(res.body.total).to.equal(0) | ||
152 | expect(res.body.data).to.be.an('array') | ||
153 | expect(res.body.data.length).to.equal(0) | ||
154 | }) | ||
155 | |||
156 | it('Should remove a video from the blacklist on server 1', async function () { | ||
157 | // Get one video in the blacklist | ||
158 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
159 | videoToRemove = res.body.data[0] | ||
160 | blacklist = res.body.data.slice(1) | ||
161 | |||
162 | // Remove it | ||
163 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoToRemove.video.id) | ||
164 | }) | ||
165 | |||
166 | it('Should have the ex-blacklisted video in videos list on server 1', async function () { | ||
167 | const res = await getVideosList(servers[0].url) | ||
168 | expect(res.body.total).to.equal(1) | ||
169 | |||
170 | const videos = res.body.data | ||
171 | expect(videos).to.be.an('array') | ||
172 | expect(videos.length).to.equal(1) | ||
173 | |||
174 | expect(videos[0].name).to.equal(videoToRemove.video.name) | ||
175 | expect(videos[0].id).to.equal(videoToRemove.video.id) | ||
176 | }) | ||
177 | |||
178 | it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () { | ||
179 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
180 | expect(res.body.total).to.equal(1) | ||
181 | |||
182 | const videos = res.body.data | ||
183 | expect(videos).to.be.an('array') | ||
184 | expect(videos.length).to.equal(1) | ||
185 | expect(videos).to.deep.equal(blacklist) | ||
186 | }) | ||
187 | }) | ||
188 | |||
189 | after(async function () { | ||
190 | killallServers(servers) | ||
191 | }) | ||
192 | }) | ||
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 41429a3d8..63514d69c 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts | |||
@@ -13,7 +13,7 @@ import { | |||
13 | updateVideoChannelAvatar, | 13 | updateVideoChannelAvatar, |
14 | uploadVideo, | 14 | uploadVideo, |
15 | userLogin | 15 | userLogin |
16 | } from '../../utils' | 16 | } from '../../../../shared/utils' |
17 | import { | 17 | import { |
18 | addVideoChannel, | 18 | addVideoChannel, |
19 | deleteVideoChannel, | 19 | deleteVideoChannel, |
@@ -26,8 +26,8 @@ import { | |||
26 | ServerInfo, | 26 | ServerInfo, |
27 | setAccessTokensToServers, | 27 | setAccessTokensToServers, |
28 | updateVideoChannel | 28 | updateVideoChannel |
29 | } from '../../utils/index' | 29 | } from '../../../../shared/utils/index' |
30 | import { waitJobs } from '../../utils/server/jobs' | 30 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
31 | 31 | ||
32 | const expect = chai.expect | 32 | const expect = chai.expect |
33 | 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-imports.ts b/server/tests/api/videos/video-imports.ts index aaee79a4a..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 | ||
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 b226a9d50..632c4244c 100644 --- a/server/tests/api/videos/video-schedule-update.ts +++ b/server/tests/api/videos/video-schedule-update.ts | |||
@@ -15,8 +15,8 @@ import { | |||
15 | updateVideo, | 15 | updateVideo, |
16 | uploadVideo, | 16 | uploadVideo, |
17 | wait | 17 | 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/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 23920d452..eefd32ef8 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -19,9 +19,9 @@ 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 | import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' |
26 | 26 | ||
27 | const expect = chai.expect | 27 | const expect = chai.expect |
@@ -321,6 +321,34 @@ describe('Test video transcoding', function () { | |||
321 | } | 321 | } |
322 | }) | 322 | }) |
323 | 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 | |||
324 | after(async function () { | 352 | after(async function () { |
325 | killallServers(servers) | 353 | killallServers(servers) |
326 | }) | 354 | }) |
diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts index a7588129f..59e37ad86 100644 --- a/server/tests/api/videos/videos-filter.ts +++ b/server/tests/api/videos/videos-filter.ts | |||
@@ -13,7 +13,7 @@ import { | |||
13 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | uploadVideo, | 14 | uploadVideo, |
15 | userLogin | 15 | userLogin |
16 | } from '../../utils' | 16 | } from '../../../../shared/utils' |
17 | import { Video, VideoPrivacy } from '../../../../shared/models/videos' | 17 | import { Video, VideoPrivacy } from '../../../../shared/models/videos' |
18 | import { UserRole } from '../../../../shared/models/users' | 18 | import { UserRole } from '../../../../shared/models/users' |
19 | 19 | ||
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 index 66dd39cce..6f6bc25a6 100644 --- a/server/tests/cli/optimize-old-videos.ts +++ b/server/tests/cli/optimize-old-videos.ts | |||
@@ -15,8 +15,8 @@ import { | |||
15 | ServerInfo, | 15 | ServerInfo, |
16 | setAccessTokensToServers, | 16 | setAccessTokensToServers, |
17 | uploadVideo, viewVideo, wait | 17 | uploadVideo, viewVideo, wait |
18 | } from '../utils' | 18 | } from '../../../shared/utils' |
19 | import { waitJobs } from '../utils/server/jobs' | 19 | import { waitJobs } from '../../../shared/utils/server/jobs' |
20 | import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils' | 20 | import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils' |
21 | import { VIDEO_TRANSCODING_FPS } from '../../initializers' | 21 | import { VIDEO_TRANSCODING_FPS } from '../../initializers' |
22 | import { join } from 'path' | 22 | import { join } from 'path' |
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts index 7a712bc4e..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 |
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..811ea6a9f 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 | ||
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 index a6d829a9f..e604cf7e3 100644 --- a/server/tests/helpers/core-utils.ts +++ b/server/tests/helpers/core-utils.ts | |||
@@ -2,13 +2,16 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { snakeCase, isNumber } from 'lodash' | ||
5 | import { | 6 | import { |
6 | parseBytes | 7 | parseBytes, objectConverter |
7 | } from '../../helpers/core-utils' | 8 | } from '../../helpers/core-utils' |
9 | import { isNumeric } from 'validator' | ||
8 | 10 | ||
9 | const expect = chai.expect | 11 | const expect = chai.expect |
10 | 12 | ||
11 | describe('Parse Bytes', function () { | 13 | describe('Parse Bytes', function () { |
14 | |||
12 | it('Should pass when given valid value', async function () { | 15 | it('Should pass when given valid value', async function () { |
13 | // just return it | 16 | // just return it |
14 | expect(parseBytes(1024)).to.be.eq(1024) | 17 | expect(parseBytes(1024)).to.be.eq(1024) |
@@ -45,4 +48,51 @@ describe('Parse Bytes', function () { | |||
45 | it('Should be invalid when given invalid value', async function () { | 48 | it('Should be invalid when given invalid value', async function () { |
46 | expect(parseBytes('6GB 1GB')).to.be.eq(6) | 49 | expect(parseBytes('6GB 1GB')).to.be.eq(6) |
47 | }) | 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 | }) | ||
48 | }) | 98 | }) |
diff --git a/server/tests/helpers/index.ts b/server/tests/helpers/index.ts index 40c7dc70e..551208245 100644 --- a/server/tests/helpers/index.ts +++ b/server/tests/helpers/index.ts | |||
@@ -1 +1,2 @@ | |||
1 | import './core-utils' | 1 | import './core-utils' |
2 | import './comment-model' | ||
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 8349631c9..000000000 --- a/server/tests/utils/index.ts +++ /dev/null | |||
@@ -1,19 +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 './miscs/stubs' | ||
8 | export * from './server/follows' | ||
9 | export * from './requests/requests' | ||
10 | export * from './server/servers' | ||
11 | export * from './videos/services' | ||
12 | export * from './users/users' | ||
13 | export * from './videos/video-abuses' | ||
14 | export * from './videos/video-blacklist' | ||
15 | export * from './videos/video-channels' | ||
16 | export * from './videos/videos' | ||
17 | export * from './videos/video-change-ownership' | ||
18 | export * from './feeds/feeds' | ||
19 | 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 589daa420..000000000 --- a/server/tests/utils/miscs/miscs.ts +++ /dev/null | |||
@@ -1,101 +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 { pathExists, readFile } from 'fs-extra' | ||
8 | import * as ffmpeg from 'fluent-ffmpeg' | ||
9 | |||
10 | const expect = chai.expect | ||
11 | let webtorrent = new WebTorrent() | ||
12 | |||
13 | function immutableAssign <T, U> (target: T, source: U) { | ||
14 | return Object.assign<{}, T, U>({}, target, source) | ||
15 | } | ||
16 | |||
17 | // Default interval -> 5 minutes | ||
18 | function dateIsValid (dateString: string, interval = 300000) { | ||
19 | const dateToCheck = new Date(dateString) | ||
20 | const now = new Date() | ||
21 | |||
22 | return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval | ||
23 | } | ||
24 | |||
25 | function wait (milliseconds: number) { | ||
26 | return new Promise(resolve => setTimeout(resolve, milliseconds)) | ||
27 | } | ||
28 | |||
29 | function webtorrentAdd (torrent: string, refreshWebTorrent = false) { | ||
30 | if (refreshWebTorrent === true) webtorrent = new WebTorrent() | ||
31 | |||
32 | return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res)) | ||
33 | } | ||
34 | |||
35 | function root () { | ||
36 | // We are in server/tests/utils/miscs | ||
37 | return join(__dirname, '..', '..', '..', '..') | ||
38 | } | ||
39 | |||
40 | async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') { | ||
41 | const res = await request(url) | ||
42 | .get(imagePath) | ||
43 | .expect(200) | ||
44 | |||
45 | const body = res.body | ||
46 | |||
47 | const data = await readFile(join(__dirname, '..', '..', 'fixtures', imageName + extension)) | ||
48 | const minLength = body.length - ((20 * body.length) / 100) | ||
49 | const maxLength = body.length + ((20 * body.length) / 100) | ||
50 | |||
51 | expect(data.length).to.be.above(minLength) | ||
52 | expect(data.length).to.be.below(maxLength) | ||
53 | } | ||
54 | |||
55 | function buildAbsoluteFixturePath (path: string, customTravisPath = false) { | ||
56 | if (isAbsolute(path)) { | ||
57 | return path | ||
58 | } | ||
59 | |||
60 | if (customTravisPath && process.env.TRAVIS) return join(process.env.HOME, 'fixtures', path) | ||
61 | |||
62 | return join(__dirname, '..', '..', 'fixtures', path) | ||
63 | } | ||
64 | |||
65 | async function generateHighBitrateVideo () { | ||
66 | const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) | ||
67 | |||
68 | const exists = await pathExists(tempFixturePath) | ||
69 | if (!exists) { | ||
70 | |||
71 | // Generate a random, high bitrate video on the fly, so we don't have to include | ||
72 | // a large file in the repo. The video needs to have a certain minimum length so | ||
73 | // that FFmpeg properly applies bitrate limits. | ||
74 | // https://stackoverflow.com/a/15795112 | ||
75 | return new Promise<string>(async (res, rej) => { | ||
76 | ffmpeg() | ||
77 | .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ]) | ||
78 | .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ]) | ||
79 | .outputOptions([ '-maxrate 10M', '-bufsize 10M' ]) | ||
80 | .output(tempFixturePath) | ||
81 | .on('error', rej) | ||
82 | .on('end', () => res(tempFixturePath)) | ||
83 | .run() | ||
84 | }) | ||
85 | } | ||
86 | |||
87 | return tempFixturePath | ||
88 | } | ||
89 | |||
90 | // --------------------------------------------------------------------------- | ||
91 | |||
92 | export { | ||
93 | dateIsValid, | ||
94 | wait, | ||
95 | webtorrentAdd, | ||
96 | immutableAssign, | ||
97 | testImage, | ||
98 | buildAbsoluteFixturePath, | ||
99 | root, | ||
100 | generateHighBitrateVideo | ||
101 | } | ||
diff --git a/server/tests/utils/miscs/sql.ts b/server/tests/utils/miscs/sql.ts deleted file mode 100644 index 027f78131..000000000 --- a/server/tests/utils/miscs/sql.ts +++ /dev/null | |||
@@ -1,38 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | function getSequelize (serverNumber: number) { | ||
4 | const dbname = 'peertube_test' + serverNumber | ||
5 | const username = 'peertube' | ||
6 | const password = 'peertube' | ||
7 | const host = 'localhost' | ||
8 | const port = 5432 | ||
9 | |||
10 | return new Sequelize(dbname, username, password, { | ||
11 | dialect: 'postgres', | ||
12 | host, | ||
13 | port, | ||
14 | operatorsAliases: false, | ||
15 | logging: false | ||
16 | }) | ||
17 | } | ||
18 | |||
19 | function setActorField (serverNumber: number, to: string, field: string, value: string) { | ||
20 | const seq = getSequelize(serverNumber) | ||
21 | |||
22 | const options = { type: Sequelize.QueryTypes.UPDATE } | ||
23 | |||
24 | return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options) | ||
25 | } | ||
26 | |||
27 | function setVideoField (serverNumber: number, uuid: string, field: string, value: string) { | ||
28 | const seq = getSequelize(serverNumber) | ||
29 | |||
30 | const options = { type: Sequelize.QueryTypes.UPDATE } | ||
31 | |||
32 | return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options) | ||
33 | } | ||
34 | |||
35 | export { | ||
36 | setVideoField, | ||
37 | setActorField | ||
38 | } | ||
diff --git a/server/tests/utils/miscs/stubs.ts b/server/tests/utils/miscs/stubs.ts deleted file mode 100644 index d1eb0e3b2..000000000 --- a/server/tests/utils/miscs/stubs.ts +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | function buildRequestStub (): any { | ||
2 | return { } | ||
3 | } | ||
4 | |||
5 | function buildResponseStub (): any { | ||
6 | return { | ||
7 | locals: {} | ||
8 | } | ||
9 | } | ||
10 | |||
11 | export { | ||
12 | buildResponseStub, | ||
13 | buildRequestStub | ||
14 | } | ||
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/activitypub.ts b/server/tests/utils/requests/activitypub.ts deleted file mode 100644 index 96fee60a8..000000000 --- a/server/tests/utils/requests/activitypub.ts +++ /dev/null | |||
@@ -1,43 +0,0 @@ | |||
1 | import { doRequest } from '../../../helpers/requests' | ||
2 | import { HTTP_SIGNATURE } from '../../../initializers' | ||
3 | import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' | ||
4 | import { activityPubContextify } from '../../../helpers/activitypub' | ||
5 | |||
6 | function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { | ||
7 | const options = { | ||
8 | method: 'POST', | ||
9 | uri: url, | ||
10 | json: body, | ||
11 | httpSignature, | ||
12 | headers | ||
13 | } | ||
14 | |||
15 | return doRequest(options) | ||
16 | } | ||
17 | |||
18 | async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) { | ||
19 | const follow = { | ||
20 | type: 'Follow', | ||
21 | id: by.url + '/toto', | ||
22 | actor: by.url, | ||
23 | object: to.url | ||
24 | } | ||
25 | |||
26 | const body = activityPubContextify(follow) | ||
27 | |||
28 | const httpSignature = { | ||
29 | algorithm: HTTP_SIGNATURE.ALGORITHM, | ||
30 | authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, | ||
31 | keyId: by.url, | ||
32 | key: by.privateKey, | ||
33 | headers: HTTP_SIGNATURE.HEADERS_TO_SIGN | ||
34 | } | ||
35 | const headers = buildGlobalHeaders(body) | ||
36 | |||
37 | return makePOSTAPRequest(to.url, body, httpSignature, headers) | ||
38 | } | ||
39 | |||
40 | export { | ||
41 | makePOSTAPRequest, | ||
42 | makeFollowRequest | ||
43 | } | ||
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 a2a549682..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 '../miscs/miscs' | ||
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 5796540f7..000000000 --- a/server/tests/utils/requests/requests.ts +++ /dev/null | |||
@@ -1,168 +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.expect(options.statusCodeExpected) | ||
41 | } | ||
42 | |||
43 | function makeUploadRequest (options: { | ||
44 | url: string, | ||
45 | method?: 'POST' | 'PUT', | ||
46 | path: string, | ||
47 | token?: string, | ||
48 | fields: { [ fieldName: string ]: any }, | ||
49 | attaches: { [ attachName: string ]: any | any[] }, | ||
50 | statusCodeExpected?: number | ||
51 | }) { | ||
52 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 | ||
53 | |||
54 | let req: request.Test | ||
55 | if (options.method === 'PUT') { | ||
56 | req = request(options.url).put(options.path) | ||
57 | } else { | ||
58 | req = request(options.url).post(options.path) | ||
59 | } | ||
60 | |||
61 | req.set('Accept', 'application/json') | ||
62 | |||
63 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | ||
64 | |||
65 | Object.keys(options.fields).forEach(field => { | ||
66 | const value = options.fields[field] | ||
67 | |||
68 | if (Array.isArray(value)) { | ||
69 | for (let i = 0; i < value.length; i++) { | ||
70 | req.field(field + '[' + i + ']', value[i]) | ||
71 | } | ||
72 | } else { | ||
73 | req.field(field, value) | ||
74 | } | ||
75 | }) | ||
76 | |||
77 | Object.keys(options.attaches).forEach(attach => { | ||
78 | const value = options.attaches[attach] | ||
79 | if (Array.isArray(value)) { | ||
80 | req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1]) | ||
81 | } else { | ||
82 | req.attach(attach, buildAbsoluteFixturePath(value)) | ||
83 | } | ||
84 | }) | ||
85 | |||
86 | return req.expect(options.statusCodeExpected) | ||
87 | } | ||
88 | |||
89 | function makePostBodyRequest (options: { | ||
90 | url: string, | ||
91 | path: string, | ||
92 | token?: string, | ||
93 | fields?: { [ fieldName: string ]: any }, | ||
94 | statusCodeExpected?: number | ||
95 | }) { | ||
96 | if (!options.fields) options.fields = {} | ||
97 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 | ||
98 | |||
99 | const req = request(options.url) | ||
100 | .post(options.path) | ||
101 | .set('Accept', 'application/json') | ||
102 | |||
103 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | ||
104 | |||
105 | return req.send(options.fields) | ||
106 | .expect(options.statusCodeExpected) | ||
107 | } | ||
108 | |||
109 | function makePutBodyRequest (options: { | ||
110 | url: string, | ||
111 | path: string, | ||
112 | token?: string, | ||
113 | fields: { [ fieldName: string ]: any }, | ||
114 | statusCodeExpected?: number | ||
115 | }) { | ||
116 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 | ||
117 | |||
118 | const req = request(options.url) | ||
119 | .put(options.path) | ||
120 | .set('Accept', 'application/json') | ||
121 | |||
122 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | ||
123 | |||
124 | return req.send(options.fields) | ||
125 | .expect(options.statusCodeExpected) | ||
126 | } | ||
127 | |||
128 | function makeHTMLRequest (url: string, path: string) { | ||
129 | return request(url) | ||
130 | .get(path) | ||
131 | .set('Accept', 'text/html') | ||
132 | .expect(200) | ||
133 | } | ||
134 | |||
135 | function updateAvatarRequest (options: { | ||
136 | url: string, | ||
137 | path: string, | ||
138 | accessToken: string, | ||
139 | fixture: string | ||
140 | }) { | ||
141 | let filePath = '' | ||
142 | if (isAbsolute(options.fixture)) { | ||
143 | filePath = options.fixture | ||
144 | } else { | ||
145 | filePath = join(__dirname, '..', '..', 'fixtures', options.fixture) | ||
146 | } | ||
147 | |||
148 | return makeUploadRequest({ | ||
149 | url: options.url, | ||
150 | path: options.path, | ||
151 | token: options.accessToken, | ||
152 | fields: {}, | ||
153 | attaches: { avatarfile: filePath }, | ||
154 | statusCodeExpected: 200 | ||
155 | }) | ||
156 | } | ||
157 | |||
158 | // --------------------------------------------------------------------------- | ||
159 | |||
160 | export { | ||
161 | makeHTMLRequest, | ||
162 | makeGetRequest, | ||
163 | makeUploadRequest, | ||
164 | makePostBodyRequest, | ||
165 | makePutBodyRequest, | ||
166 | makeDeleteRequest, | ||
167 | updateAvatarRequest | ||
168 | } | ||
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 8c0037ccc..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 '../miscs/miscs' | ||
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 eccb198ca..000000000 --- a/server/tests/utils/server/activitypub.ts +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | |||
3 | function makeActivityPubGetRequest (url: string, path: string, expectedStatus = 200) { | ||
4 | return request(url) | ||
5 | .get(path) | ||
6 | .set('Accept', 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8') | ||
7 | .expect(expectedStatus) | ||
8 | } | ||
9 | |||
10 | // --------------------------------------------------------------------------- | ||
11 | |||
12 | export { | ||
13 | makeActivityPubGetRequest | ||
14 | } | ||
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 aa3100d34..000000000 --- a/server/tests/utils/server/config.ts +++ /dev/null | |||
@@ -1,135 +0,0 @@ | |||
1 | import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests' | ||
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 7741757a6..000000000 --- a/server/tests/utils/server/follows.ts +++ /dev/null | |||
@@ -1,79 +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, search?: 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 | .query({ search }) | ||
14 | .set('Accept', 'application/json') | ||
15 | .expect(200) | ||
16 | .expect('Content-Type', /json/) | ||
17 | } | ||
18 | |||
19 | function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) { | ||
20 | const path = '/api/v1/server/following' | ||
21 | |||
22 | return request(url) | ||
23 | .get(path) | ||
24 | .query({ start }) | ||
25 | .query({ count }) | ||
26 | .query({ sort }) | ||
27 | .query({ search }) | ||
28 | .set('Accept', 'application/json') | ||
29 | .expect(200) | ||
30 | .expect('Content-Type', /json/) | ||
31 | } | ||
32 | |||
33 | async function follow (follower: string, following: string[], accessToken: string, expectedStatus = 204) { | ||
34 | const path = '/api/v1/server/following' | ||
35 | |||
36 | const followingHosts = following.map(f => f.replace(/^http:\/\//, '')) | ||
37 | const res = await request(follower) | ||
38 | .post(path) | ||
39 | .set('Accept', 'application/json') | ||
40 | .set('Authorization', 'Bearer ' + accessToken) | ||
41 | .send({ 'hosts': followingHosts }) | ||
42 | .expect(expectedStatus) | ||
43 | |||
44 | return res | ||
45 | } | ||
46 | |||
47 | async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) { | ||
48 | const path = '/api/v1/server/following/' + target.host | ||
49 | |||
50 | const res = await request(url) | ||
51 | .delete(path) | ||
52 | .set('Accept', 'application/json') | ||
53 | .set('Authorization', 'Bearer ' + accessToken) | ||
54 | .expect(expectedStatus) | ||
55 | |||
56 | return res | ||
57 | } | ||
58 | |||
59 | async function doubleFollow (server1: ServerInfo, server2: ServerInfo) { | ||
60 | await Promise.all([ | ||
61 | follow(server1.url, [ server2.url ], server1.accessToken), | ||
62 | follow(server2.url, [ server1.url ], server2.accessToken) | ||
63 | ]) | ||
64 | |||
65 | // Wait request propagation | ||
66 | await waitJobs([ server1, server2 ]) | ||
67 | |||
68 | return true | ||
69 | } | ||
70 | |||
71 | // --------------------------------------------------------------------------- | ||
72 | |||
73 | export { | ||
74 | getFollowersListPaginationAndSort, | ||
75 | getFollowingListPaginationAndSort, | ||
76 | unfollow, | ||
77 | follow, | ||
78 | doubleFollow | ||
79 | } | ||
diff --git a/server/tests/utils/server/jobs.ts b/server/tests/utils/server/jobs.ts deleted file mode 100644 index 26180ec72..000000000 --- a/server/tests/utils/server/jobs.ts +++ /dev/null | |||
@@ -1,78 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import { Job, JobState } from '../../../../shared/models' | ||
3 | import { ServerInfo } from './servers' | ||
4 | import { wait } from '../miscs/miscs' | ||
5 | |||
6 | function getJobsList (url: string, accessToken: string, state: JobState) { | ||
7 | const path = '/api/v1/jobs/' + state | ||
8 | |||
9 | return request(url) | ||
10 | .get(path) | ||
11 | .set('Accept', 'application/json') | ||
12 | .set('Authorization', 'Bearer ' + accessToken) | ||
13 | .expect(200) | ||
14 | .expect('Content-Type', /json/) | ||
15 | } | ||
16 | |||
17 | function getJobsListPaginationAndSort (url: string, accessToken: string, state: JobState, start: number, count: number, sort: string) { | ||
18 | const path = '/api/v1/jobs/' + state | ||
19 | |||
20 | return request(url) | ||
21 | .get(path) | ||
22 | .query({ start }) | ||
23 | .query({ count }) | ||
24 | .query({ sort }) | ||
25 | .set('Accept', 'application/json') | ||
26 | .set('Authorization', 'Bearer ' + accessToken) | ||
27 | .expect(200) | ||
28 | .expect('Content-Type', /json/) | ||
29 | } | ||
30 | |||
31 | async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { | ||
32 | let servers: ServerInfo[] | ||
33 | |||
34 | if (Array.isArray(serversArg) === false) servers = [ serversArg as ServerInfo ] | ||
35 | else servers = serversArg as ServerInfo[] | ||
36 | |||
37 | const states: JobState[] = [ 'waiting', 'active', 'delayed' ] | ||
38 | const tasks: Promise<any>[] = [] | ||
39 | let pendingRequests: boolean | ||
40 | |||
41 | do { | ||
42 | pendingRequests = false | ||
43 | |||
44 | // Check if each server has pending request | ||
45 | for (const server of servers) { | ||
46 | for (const state of states) { | ||
47 | const p = getJobsListPaginationAndSort(server.url, server.accessToken, state, 0, 10, '-createdAt') | ||
48 | .then(res => res.body.data) | ||
49 | .then((jobs: Job[]) => jobs.filter(j => j.type !== 'videos-views')) | ||
50 | .then(jobs => { | ||
51 | if (jobs.length !== 0) pendingRequests = true | ||
52 | }) | ||
53 | tasks.push(p) | ||
54 | } | ||
55 | } | ||
56 | |||
57 | await Promise.all(tasks) | ||
58 | |||
59 | // Retry, in case of new jobs were created | ||
60 | if (pendingRequests === false) { | ||
61 | await wait(1000) | ||
62 | |||
63 | await Promise.all(tasks) | ||
64 | } | ||
65 | |||
66 | if (pendingRequests) { | ||
67 | await wait(1000) | ||
68 | } | ||
69 | } while (pendingRequests) | ||
70 | } | ||
71 | |||
72 | // --------------------------------------------------------------------------- | ||
73 | |||
74 | export { | ||
75 | getJobsList, | ||
76 | waitJobs, | ||
77 | getJobsListPaginationAndSort | ||
78 | } | ||
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 f358a21f1..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, args = []) { | ||
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'), args, 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 6f079ad18..000000000 --- a/server/tests/utils/server/stats.ts +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | import { makeGetRequest } from '../requests/requests' | ||
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 257fa5b27..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 '../miscs/miscs' | ||
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/blocklist.ts b/server/tests/utils/users/blocklist.ts deleted file mode 100644 index 5feb84179..000000000 --- a/server/tests/utils/users/blocklist.ts +++ /dev/null | |||
@@ -1,197 +0,0 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import { makeGetRequest, makeDeleteRequest, makePostBodyRequest } from '../requests/requests' | ||
4 | |||
5 | function getAccountBlocklistByAccount ( | ||
6 | url: string, | ||
7 | token: string, | ||
8 | start: number, | ||
9 | count: number, | ||
10 | sort = '-createdAt', | ||
11 | statusCodeExpected = 200 | ||
12 | ) { | ||
13 | const path = '/api/v1/users/me/blocklist/accounts' | ||
14 | |||
15 | return makeGetRequest({ | ||
16 | url, | ||
17 | token, | ||
18 | query: { start, count, sort }, | ||
19 | path, | ||
20 | statusCodeExpected | ||
21 | }) | ||
22 | } | ||
23 | |||
24 | function addAccountToAccountBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) { | ||
25 | const path = '/api/v1/users/me/blocklist/accounts' | ||
26 | |||
27 | return makePostBodyRequest({ | ||
28 | url, | ||
29 | path, | ||
30 | token, | ||
31 | fields: { | ||
32 | accountName: accountToBlock | ||
33 | }, | ||
34 | statusCodeExpected | ||
35 | }) | ||
36 | } | ||
37 | |||
38 | function removeAccountFromAccountBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) { | ||
39 | const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock | ||
40 | |||
41 | return makeDeleteRequest({ | ||
42 | url, | ||
43 | path, | ||
44 | token, | ||
45 | statusCodeExpected | ||
46 | }) | ||
47 | } | ||
48 | |||
49 | function getServerBlocklistByAccount ( | ||
50 | url: string, | ||
51 | token: string, | ||
52 | start: number, | ||
53 | count: number, | ||
54 | sort = '-createdAt', | ||
55 | statusCodeExpected = 200 | ||
56 | ) { | ||
57 | const path = '/api/v1/users/me/blocklist/servers' | ||
58 | |||
59 | return makeGetRequest({ | ||
60 | url, | ||
61 | token, | ||
62 | query: { start, count, sort }, | ||
63 | path, | ||
64 | statusCodeExpected | ||
65 | }) | ||
66 | } | ||
67 | |||
68 | function addServerToAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { | ||
69 | const path = '/api/v1/users/me/blocklist/servers' | ||
70 | |||
71 | return makePostBodyRequest({ | ||
72 | url, | ||
73 | path, | ||
74 | token, | ||
75 | fields: { | ||
76 | host: serverToBlock | ||
77 | }, | ||
78 | statusCodeExpected | ||
79 | }) | ||
80 | } | ||
81 | |||
82 | function removeServerFromAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { | ||
83 | const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock | ||
84 | |||
85 | return makeDeleteRequest({ | ||
86 | url, | ||
87 | path, | ||
88 | token, | ||
89 | statusCodeExpected | ||
90 | }) | ||
91 | } | ||
92 | |||
93 | function getAccountBlocklistByServer ( | ||
94 | url: string, | ||
95 | token: string, | ||
96 | start: number, | ||
97 | count: number, | ||
98 | sort = '-createdAt', | ||
99 | statusCodeExpected = 200 | ||
100 | ) { | ||
101 | const path = '/api/v1/server/blocklist/accounts' | ||
102 | |||
103 | return makeGetRequest({ | ||
104 | url, | ||
105 | token, | ||
106 | query: { start, count, sort }, | ||
107 | path, | ||
108 | statusCodeExpected | ||
109 | }) | ||
110 | } | ||
111 | |||
112 | function addAccountToServerBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) { | ||
113 | const path = '/api/v1/server/blocklist/accounts' | ||
114 | |||
115 | return makePostBodyRequest({ | ||
116 | url, | ||
117 | path, | ||
118 | token, | ||
119 | fields: { | ||
120 | accountName: accountToBlock | ||
121 | }, | ||
122 | statusCodeExpected | ||
123 | }) | ||
124 | } | ||
125 | |||
126 | function removeAccountFromServerBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) { | ||
127 | const path = '/api/v1/server/blocklist/accounts/' + accountToUnblock | ||
128 | |||
129 | return makeDeleteRequest({ | ||
130 | url, | ||
131 | path, | ||
132 | token, | ||
133 | statusCodeExpected | ||
134 | }) | ||
135 | } | ||
136 | |||
137 | function getServerBlocklistByServer ( | ||
138 | url: string, | ||
139 | token: string, | ||
140 | start: number, | ||
141 | count: number, | ||
142 | sort = '-createdAt', | ||
143 | statusCodeExpected = 200 | ||
144 | ) { | ||
145 | const path = '/api/v1/server/blocklist/servers' | ||
146 | |||
147 | return makeGetRequest({ | ||
148 | url, | ||
149 | token, | ||
150 | query: { start, count, sort }, | ||
151 | path, | ||
152 | statusCodeExpected | ||
153 | }) | ||
154 | } | ||
155 | |||
156 | function addServerToServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { | ||
157 | const path = '/api/v1/server/blocklist/servers' | ||
158 | |||
159 | return makePostBodyRequest({ | ||
160 | url, | ||
161 | path, | ||
162 | token, | ||
163 | fields: { | ||
164 | host: serverToBlock | ||
165 | }, | ||
166 | statusCodeExpected | ||
167 | }) | ||
168 | } | ||
169 | |||
170 | function removeServerFromServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { | ||
171 | const path = '/api/v1/server/blocklist/servers/' + serverToBlock | ||
172 | |||
173 | return makeDeleteRequest({ | ||
174 | url, | ||
175 | path, | ||
176 | token, | ||
177 | statusCodeExpected | ||
178 | }) | ||
179 | } | ||
180 | |||
181 | // --------------------------------------------------------------------------- | ||
182 | |||
183 | export { | ||
184 | getAccountBlocklistByAccount, | ||
185 | addAccountToAccountBlocklist, | ||
186 | removeAccountFromAccountBlocklist, | ||
187 | getServerBlocklistByAccount, | ||
188 | addServerToAccountBlocklist, | ||
189 | removeServerFromAccountBlocklist, | ||
190 | |||
191 | getAccountBlocklistByServer, | ||
192 | addAccountToServerBlocklist, | ||
193 | removeAccountFromServerBlocklist, | ||
194 | getServerBlocklistByServer, | ||
195 | addServerToServerBlocklist, | ||
196 | removeServerFromServerBlocklist | ||
197 | } | ||
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 7148fbfca..000000000 --- a/server/tests/utils/users/user-subscriptions.ts +++ /dev/null | |||
@@ -1,82 +0,0 @@ | |||
1 | import { makeDeleteRequest, makeGetRequest, makePostBodyRequest } from '../requests/requests' | ||
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 f12992315..000000000 --- a/server/tests/utils/users/users.ts +++ /dev/null | |||
@@ -1,298 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests' | ||
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, search?: 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 | .query({ search }) | ||
124 | .set('Accept', 'application/json') | ||
125 | .set('Authorization', 'Bearer ' + accessToken) | ||
126 | .expect(200) | ||
127 | .expect('Content-Type', /json/) | ||
128 | } | ||
129 | |||
130 | function removeUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) { | ||
131 | const path = '/api/v1/users' | ||
132 | |||
133 | return request(url) | ||
134 | .delete(path + '/' + userId) | ||
135 | .set('Accept', 'application/json') | ||
136 | .set('Authorization', 'Bearer ' + accessToken) | ||
137 | .expect(expectedStatus) | ||
138 | } | ||
139 | |||
140 | function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204, reason?: string) { | ||
141 | const path = '/api/v1/users' | ||
142 | let body: any | ||
143 | if (reason) body = { reason } | ||
144 | |||
145 | return request(url) | ||
146 | .post(path + '/' + userId + '/block') | ||
147 | .send(body) | ||
148 | .set('Accept', 'application/json') | ||
149 | .set('Authorization', 'Bearer ' + accessToken) | ||
150 | .expect(expectedStatus) | ||
151 | } | ||
152 | |||
153 | function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) { | ||
154 | const path = '/api/v1/users' | ||
155 | |||
156 | return request(url) | ||
157 | .post(path + '/' + userId + '/unblock') | ||
158 | .set('Accept', 'application/json') | ||
159 | .set('Authorization', 'Bearer ' + accessToken) | ||
160 | .expect(expectedStatus) | ||
161 | } | ||
162 | |||
163 | function updateMyUser (options: { | ||
164 | url: string | ||
165 | accessToken: string, | ||
166 | currentPassword?: string, | ||
167 | newPassword?: string, | ||
168 | nsfwPolicy?: NSFWPolicyType, | ||
169 | email?: string, | ||
170 | autoPlayVideo?: boolean | ||
171 | displayName?: string, | ||
172 | description?: string | ||
173 | }) { | ||
174 | const path = '/api/v1/users/me' | ||
175 | |||
176 | const toSend = {} | ||
177 | if (options.currentPassword !== undefined && options.currentPassword !== null) toSend['currentPassword'] = options.currentPassword | ||
178 | if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword | ||
179 | if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend['nsfwPolicy'] = options.nsfwPolicy | ||
180 | if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo | ||
181 | if (options.email !== undefined && options.email !== null) toSend['email'] = options.email | ||
182 | if (options.description !== undefined && options.description !== null) toSend['description'] = options.description | ||
183 | if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName | ||
184 | |||
185 | return makePutBodyRequest({ | ||
186 | url: options.url, | ||
187 | path, | ||
188 | token: options.accessToken, | ||
189 | fields: toSend, | ||
190 | statusCodeExpected: 204 | ||
191 | }) | ||
192 | } | ||
193 | |||
194 | function updateMyAvatar (options: { | ||
195 | url: string, | ||
196 | accessToken: string, | ||
197 | fixture: string | ||
198 | }) { | ||
199 | const path = '/api/v1/users/me/avatar/pick' | ||
200 | |||
201 | return updateAvatarRequest(Object.assign(options, { path })) | ||
202 | } | ||
203 | |||
204 | function updateUser (options: { | ||
205 | url: string | ||
206 | userId: number, | ||
207 | accessToken: string, | ||
208 | email?: string, | ||
209 | emailVerified?: boolean, | ||
210 | videoQuota?: number, | ||
211 | videoQuotaDaily?: number, | ||
212 | role?: UserRole | ||
213 | }) { | ||
214 | const path = '/api/v1/users/' + options.userId | ||
215 | |||
216 | const toSend = {} | ||
217 | if (options.email !== undefined && options.email !== null) toSend['email'] = options.email | ||
218 | if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified | ||
219 | if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota | ||
220 | if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily | ||
221 | if (options.role !== undefined && options.role !== null) toSend['role'] = options.role | ||
222 | |||
223 | return makePutBodyRequest({ | ||
224 | url: options.url, | ||
225 | path, | ||
226 | token: options.accessToken, | ||
227 | fields: toSend, | ||
228 | statusCodeExpected: 204 | ||
229 | }) | ||
230 | } | ||
231 | |||
232 | function askResetPassword (url: string, email: string) { | ||
233 | const path = '/api/v1/users/ask-reset-password' | ||
234 | |||
235 | return makePostBodyRequest({ | ||
236 | url, | ||
237 | path, | ||
238 | fields: { email }, | ||
239 | statusCodeExpected: 204 | ||
240 | }) | ||
241 | } | ||
242 | |||
243 | function resetPassword (url: string, userId: number, verificationString: string, password: string, statusCodeExpected = 204) { | ||
244 | const path = '/api/v1/users/' + userId + '/reset-password' | ||
245 | |||
246 | return makePostBodyRequest({ | ||
247 | url, | ||
248 | path, | ||
249 | fields: { password, verificationString }, | ||
250 | statusCodeExpected | ||
251 | }) | ||
252 | } | ||
253 | |||
254 | function askSendVerifyEmail (url: string, email: string) { | ||
255 | const path = '/api/v1/users/ask-send-verify-email' | ||
256 | |||
257 | return makePostBodyRequest({ | ||
258 | url, | ||
259 | path, | ||
260 | fields: { email }, | ||
261 | statusCodeExpected: 204 | ||
262 | }) | ||
263 | } | ||
264 | |||
265 | function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) { | ||
266 | const path = '/api/v1/users/' + userId + '/verify-email' | ||
267 | |||
268 | return makePostBodyRequest({ | ||
269 | url, | ||
270 | path, | ||
271 | fields: { verificationString }, | ||
272 | statusCodeExpected | ||
273 | }) | ||
274 | } | ||
275 | |||
276 | // --------------------------------------------------------------------------- | ||
277 | |||
278 | export { | ||
279 | createUser, | ||
280 | registerUser, | ||
281 | getMyUserInformation, | ||
282 | getMyUserVideoRating, | ||
283 | deleteMe, | ||
284 | getMyUserVideoQuotaUsed, | ||
285 | getUsersList, | ||
286 | getUsersListPaginationAndSort, | ||
287 | removeUser, | ||
288 | updateUser, | ||
289 | updateMyUser, | ||
290 | getUserInformation, | ||
291 | blockUser, | ||
292 | unblockUser, | ||
293 | askResetPassword, | ||
294 | resetPassword, | ||
295 | updateMyAvatar, | ||
296 | askSendVerifyEmail, | ||
297 | verifyEmail | ||
298 | } | ||
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 4ad82ad8c..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 '../requests/requests' | ||
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 8d67f617b..000000000 --- a/server/tests/utils/videos/video-captions.ts +++ /dev/null | |||
@@ -1,71 +0,0 @@ | |||
1 | import { makeDeleteRequest, makeGetRequest, makeUploadRequest } from '../requests/requests' | ||
2 | import * as request from 'supertest' | ||
3 | import * as chai from 'chai' | ||
4 | import { buildAbsoluteFixturePath } from '../miscs/miscs' | ||
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 70e8d1a6b..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 '../requests/requests' | ||
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 0ebf69ced..000000000 --- a/server/tests/utils/videos/video-comments.ts +++ /dev/null | |||
@@ -1,87 +0,0 @@ | |||
1 | import * as request from 'supertest' | ||
2 | import { makeDeleteRequest } from '../requests/requests' | ||
3 | |||
4 | function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: 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 | if (token) req.set('Authorization', 'Bearer ' + token) | ||
14 | |||
15 | return req.set('Accept', 'application/json') | ||
16 | .expect(200) | ||
17 | .expect('Content-Type', /json/) | ||
18 | } | ||
19 | |||
20 | function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) { | ||
21 | const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId | ||
22 | |||
23 | const req = request(url) | ||
24 | .get(path) | ||
25 | .set('Accept', 'application/json') | ||
26 | |||
27 | if (token) req.set('Authorization', 'Bearer ' + token) | ||
28 | |||
29 | return req.expect(200) | ||
30 | .expect('Content-Type', /json/) | ||
31 | } | ||
32 | |||
33 | function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) { | ||
34 | const path = '/api/v1/videos/' + videoId + '/comment-threads' | ||
35 | |||
36 | return request(url) | ||
37 | .post(path) | ||
38 | .send({ text }) | ||
39 | .set('Accept', 'application/json') | ||
40 | .set('Authorization', 'Bearer ' + token) | ||
41 | .expect(expectedStatus) | ||
42 | } | ||
43 | |||
44 | function addVideoCommentReply ( | ||
45 | url: string, | ||
46 | token: string, | ||
47 | videoId: number | string, | ||
48 | inReplyToCommentId: number, | ||
49 | text: string, | ||
50 | expectedStatus = 200 | ||
51 | ) { | ||
52 | const path = '/api/v1/videos/' + videoId + '/comments/' + inReplyToCommentId | ||
53 | |||
54 | return request(url) | ||
55 | .post(path) | ||
56 | .send({ text }) | ||
57 | .set('Accept', 'application/json') | ||
58 | .set('Authorization', 'Bearer ' + token) | ||
59 | .expect(expectedStatus) | ||
60 | } | ||
61 | |||
62 | function deleteVideoComment ( | ||
63 | url: string, | ||
64 | token: string, | ||
65 | videoId: number | string, | ||
66 | commentId: number, | ||
67 | statusCodeExpected = 204 | ||
68 | ) { | ||
69 | const path = '/api/v1/videos/' + videoId + '/comments/' + commentId | ||
70 | |||
71 | return makeDeleteRequest({ | ||
72 | url, | ||
73 | path, | ||
74 | token, | ||
75 | statusCodeExpected | ||
76 | }) | ||
77 | } | ||
78 | |||
79 | // --------------------------------------------------------------------------- | ||
80 | |||
81 | export { | ||
82 | getVideoCommentThreads, | ||
83 | getVideoThreadComments, | ||
84 | addVideoCommentThread, | ||
85 | addVideoCommentReply, | ||
86 | deleteVideoComment | ||
87 | } | ||
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 eb985a5b1..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 '../requests/requests' | ||
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 d6c3e5dac..000000000 --- a/server/tests/utils/videos/videos.ts +++ /dev/null | |||
@@ -1,576 +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/constants' | ||
21 | import { dateIsValid, webtorrentAdd } from '../miscs/miscs' | ||
22 | |||
23 | type VideoAttributes = { | ||
24 | name?: string | ||
25 | category?: number | ||
26 | licence?: number | ||
27 | language?: string | ||
28 | nsfw?: boolean | ||
29 | commentsEnabled?: boolean | ||
30 | waitTranscoding?: boolean | ||
31 | description?: string | ||
32 | tags?: string[] | ||
33 | channelId?: number | ||
34 | privacy?: VideoPrivacy | ||
35 | fixture?: string | ||
36 | thumbnailfile?: string | ||
37 | previewfile?: string | ||
38 | scheduleUpdate?: { | ||
39 | updateAt: string | ||
40 | privacy?: VideoPrivacy | ||
41 | } | ||
42 | } | ||
43 | |||
44 | function getVideoCategories (url: string) { | ||
45 | const path = '/api/v1/videos/categories' | ||
46 | |||
47 | return makeGetRequest({ | ||
48 | url, | ||
49 | path, | ||
50 | statusCodeExpected: 200 | ||
51 | }) | ||
52 | } | ||
53 | |||
54 | function getVideoLicences (url: string) { | ||
55 | const path = '/api/v1/videos/licences' | ||
56 | |||
57 | return makeGetRequest({ | ||
58 | url, | ||
59 | path, | ||
60 | statusCodeExpected: 200 | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | function getVideoLanguages (url: string) { | ||
65 | const path = '/api/v1/videos/languages' | ||
66 | |||
67 | return makeGetRequest({ | ||
68 | url, | ||
69 | path, | ||
70 | statusCodeExpected: 200 | ||
71 | }) | ||
72 | } | ||
73 | |||
74 | function getVideoPrivacies (url: string) { | ||
75 | const path = '/api/v1/videos/privacies' | ||
76 | |||
77 | return makeGetRequest({ | ||
78 | url, | ||
79 | path, | ||
80 | statusCodeExpected: 200 | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | function getVideo (url: string, id: number | string, expectedStatus = 200) { | ||
85 | const path = '/api/v1/videos/' + id | ||
86 | |||
87 | return request(url) | ||
88 | .get(path) | ||
89 | .set('Accept', 'application/json') | ||
90 | .expect(expectedStatus) | ||
91 | } | ||
92 | |||
93 | function viewVideo (url: string, id: number | string, expectedStatus = 204, xForwardedFor?: string) { | ||
94 | const path = '/api/v1/videos/' + id + '/views' | ||
95 | |||
96 | const req = request(url) | ||
97 | .post(path) | ||
98 | .set('Accept', 'application/json') | ||
99 | |||
100 | if (xForwardedFor) { | ||
101 | req.set('X-Forwarded-For', xForwardedFor) | ||
102 | } | ||
103 | |||
104 | return req.expect(expectedStatus) | ||
105 | } | ||
106 | |||
107 | function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) { | ||
108 | const path = '/api/v1/videos/' + id | ||
109 | |||
110 | return request(url) | ||
111 | .get(path) | ||
112 | .set('Authorization', 'Bearer ' + token) | ||
113 | .set('Accept', 'application/json') | ||
114 | .expect(expectedStatus) | ||
115 | } | ||
116 | |||
117 | function getVideoDescription (url: string, descriptionPath: string) { | ||
118 | return request(url) | ||
119 | .get(descriptionPath) | ||
120 | .set('Accept', 'application/json') | ||
121 | .expect(200) | ||
122 | .expect('Content-Type', /json/) | ||
123 | } | ||
124 | |||
125 | function getVideosList (url: string) { | ||
126 | const path = '/api/v1/videos' | ||
127 | |||
128 | return request(url) | ||
129 | .get(path) | ||
130 | .query({ sort: 'name' }) | ||
131 | .set('Accept', 'application/json') | ||
132 | .expect(200) | ||
133 | .expect('Content-Type', /json/) | ||
134 | } | ||
135 | |||
136 | function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) { | ||
137 | const path = '/api/v1/videos' | ||
138 | |||
139 | return request(url) | ||
140 | .get(path) | ||
141 | .set('Authorization', 'Bearer ' + token) | ||
142 | .query(immutableAssign(query, { sort: 'name' })) | ||
143 | .set('Accept', 'application/json') | ||
144 | .expect(200) | ||
145 | .expect('Content-Type', /json/) | ||
146 | } | ||
147 | |||
148 | function getLocalVideos (url: string) { | ||
149 | const path = '/api/v1/videos' | ||
150 | |||
151 | return request(url) | ||
152 | .get(path) | ||
153 | .query({ sort: 'name', filter: 'local' }) | ||
154 | .set('Accept', 'application/json') | ||
155 | .expect(200) | ||
156 | .expect('Content-Type', /json/) | ||
157 | } | ||
158 | |||
159 | function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string) { | ||
160 | const path = '/api/v1/users/me/videos' | ||
161 | |||
162 | const req = request(url) | ||
163 | .get(path) | ||
164 | .query({ start: start }) | ||
165 | .query({ count: count }) | ||
166 | |||
167 | if (sort) req.query({ sort }) | ||
168 | |||
169 | return req.set('Accept', 'application/json') | ||
170 | .set('Authorization', 'Bearer ' + accessToken) | ||
171 | .expect(200) | ||
172 | .expect('Content-Type', /json/) | ||
173 | } | ||
174 | |||
175 | function getAccountVideos ( | ||
176 | url: string, | ||
177 | accessToken: string, | ||
178 | accountName: string, | ||
179 | start: number, | ||
180 | count: number, | ||
181 | sort?: string, | ||
182 | query: { nsfw?: boolean } = {} | ||
183 | ) { | ||
184 | const path = '/api/v1/accounts/' + accountName + '/videos' | ||
185 | |||
186 | return makeGetRequest({ | ||
187 | url, | ||
188 | path, | ||
189 | query: immutableAssign(query, { | ||
190 | start, | ||
191 | count, | ||
192 | sort | ||
193 | }), | ||
194 | token: accessToken, | ||
195 | statusCodeExpected: 200 | ||
196 | }) | ||
197 | } | ||
198 | |||
199 | function getVideoChannelVideos ( | ||
200 | url: string, | ||
201 | accessToken: string, | ||
202 | videoChannelName: string, | ||
203 | start: number, | ||
204 | count: number, | ||
205 | sort?: string, | ||
206 | query: { nsfw?: boolean } = {} | ||
207 | ) { | ||
208 | const path = '/api/v1/video-channels/' + videoChannelName + '/videos' | ||
209 | |||
210 | return makeGetRequest({ | ||
211 | url, | ||
212 | path, | ||
213 | query: immutableAssign(query, { | ||
214 | start, | ||
215 | count, | ||
216 | sort | ||
217 | }), | ||
218 | token: accessToken, | ||
219 | statusCodeExpected: 200 | ||
220 | }) | ||
221 | } | ||
222 | |||
223 | function getVideosListPagination (url: string, start: number, count: number, sort?: string) { | ||
224 | const path = '/api/v1/videos' | ||
225 | |||
226 | const req = request(url) | ||
227 | .get(path) | ||
228 | .query({ start: start }) | ||
229 | .query({ count: count }) | ||
230 | |||
231 | if (sort) req.query({ sort }) | ||
232 | |||
233 | return req.set('Accept', 'application/json') | ||
234 | .expect(200) | ||
235 | .expect('Content-Type', /json/) | ||
236 | } | ||
237 | |||
238 | function getVideosListSort (url: string, sort: string) { | ||
239 | const path = '/api/v1/videos' | ||
240 | |||
241 | return request(url) | ||
242 | .get(path) | ||
243 | .query({ sort: sort }) | ||
244 | .set('Accept', 'application/json') | ||
245 | .expect(200) | ||
246 | .expect('Content-Type', /json/) | ||
247 | } | ||
248 | |||
249 | function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) { | ||
250 | const path = '/api/v1/videos' | ||
251 | |||
252 | return request(url) | ||
253 | .get(path) | ||
254 | .query(query) | ||
255 | .set('Accept', 'application/json') | ||
256 | .expect(200) | ||
257 | .expect('Content-Type', /json/) | ||
258 | } | ||
259 | |||
260 | function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) { | ||
261 | const path = '/api/v1/videos' | ||
262 | |||
263 | return request(url) | ||
264 | .delete(path + '/' + id) | ||
265 | .set('Accept', 'application/json') | ||
266 | .set('Authorization', 'Bearer ' + token) | ||
267 | .expect(expectedStatus) | ||
268 | } | ||
269 | |||
270 | async function checkVideoFilesWereRemoved ( | ||
271 | videoUUID: string, | ||
272 | serverNumber: number, | ||
273 | directories = [ 'videos', 'thumbnails', 'torrents', 'previews', 'captions' ] | ||
274 | ) { | ||
275 | const testDirectory = 'test' + serverNumber | ||
276 | |||
277 | for (const directory of directories) { | ||
278 | const directoryPath = join(root(), testDirectory, directory) | ||
279 | |||
280 | const directoryExists = existsSync(directoryPath) | ||
281 | expect(directoryExists).to.be.true | ||
282 | |||
283 | const files = await readdir(directoryPath) | ||
284 | for (const file of files) { | ||
285 | expect(file).to.not.contain(videoUUID) | ||
286 | } | ||
287 | } | ||
288 | } | ||
289 | |||
290 | async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) { | ||
291 | const path = '/api/v1/videos/upload' | ||
292 | let defaultChannelId = '1' | ||
293 | |||
294 | try { | ||
295 | const res = await getMyUserInformation(url, accessToken) | ||
296 | defaultChannelId = res.body.videoChannels[0].id | ||
297 | } catch (e) { /* empty */ } | ||
298 | |||
299 | // Override default attributes | ||
300 | const attributes = Object.assign({ | ||
301 | name: 'my super video', | ||
302 | category: 5, | ||
303 | licence: 4, | ||
304 | language: 'zh', | ||
305 | channelId: defaultChannelId, | ||
306 | nsfw: true, | ||
307 | waitTranscoding: false, | ||
308 | description: 'my super description', | ||
309 | support: 'my super support text', | ||
310 | tags: [ 'tag' ], | ||
311 | privacy: VideoPrivacy.PUBLIC, | ||
312 | commentsEnabled: true, | ||
313 | fixture: 'video_short.webm' | ||
314 | }, videoAttributesArg) | ||
315 | |||
316 | const req = request(url) | ||
317 | .post(path) | ||
318 | .set('Accept', 'application/json') | ||
319 | .set('Authorization', 'Bearer ' + accessToken) | ||
320 | .field('name', attributes.name) | ||
321 | .field('nsfw', JSON.stringify(attributes.nsfw)) | ||
322 | .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled)) | ||
323 | .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding)) | ||
324 | .field('privacy', attributes.privacy.toString()) | ||
325 | .field('channelId', attributes.channelId) | ||
326 | |||
327 | if (attributes.description !== undefined) { | ||
328 | req.field('description', attributes.description) | ||
329 | } | ||
330 | if (attributes.language !== undefined) { | ||
331 | req.field('language', attributes.language.toString()) | ||
332 | } | ||
333 | if (attributes.category !== undefined) { | ||
334 | req.field('category', attributes.category.toString()) | ||
335 | } | ||
336 | if (attributes.licence !== undefined) { | ||
337 | req.field('licence', attributes.licence.toString()) | ||
338 | } | ||
339 | |||
340 | for (let i = 0; i < attributes.tags.length; i++) { | ||
341 | req.field('tags[' + i + ']', attributes.tags[i]) | ||
342 | } | ||
343 | |||
344 | if (attributes.thumbnailfile !== undefined) { | ||
345 | req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile)) | ||
346 | } | ||
347 | if (attributes.previewfile !== undefined) { | ||
348 | req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile)) | ||
349 | } | ||
350 | |||
351 | if (attributes.scheduleUpdate) { | ||
352 | req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt) | ||
353 | |||
354 | if (attributes.scheduleUpdate.privacy) { | ||
355 | req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy) | ||
356 | } | ||
357 | } | ||
358 | |||
359 | return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture)) | ||
360 | .expect(specialStatus) | ||
361 | } | ||
362 | |||
363 | function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) { | ||
364 | const path = '/api/v1/videos/' + id | ||
365 | const body = {} | ||
366 | |||
367 | if (attributes.name) body['name'] = attributes.name | ||
368 | if (attributes.category) body['category'] = attributes.category | ||
369 | if (attributes.licence) body['licence'] = attributes.licence | ||
370 | if (attributes.language) body['language'] = attributes.language | ||
371 | if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw) | ||
372 | if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled) | ||
373 | if (attributes.description) body['description'] = attributes.description | ||
374 | if (attributes.tags) body['tags'] = attributes.tags | ||
375 | if (attributes.privacy) body['privacy'] = attributes.privacy | ||
376 | if (attributes.channelId) body['channelId'] = attributes.channelId | ||
377 | if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate | ||
378 | |||
379 | // Upload request | ||
380 | if (attributes.thumbnailfile || attributes.previewfile) { | ||
381 | const attaches: any = {} | ||
382 | if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile | ||
383 | if (attributes.previewfile) attaches.previewfile = attributes.previewfile | ||
384 | |||
385 | return makeUploadRequest({ | ||
386 | url, | ||
387 | method: 'PUT', | ||
388 | path, | ||
389 | token: accessToken, | ||
390 | fields: body, | ||
391 | attaches, | ||
392 | statusCodeExpected | ||
393 | }) | ||
394 | } | ||
395 | |||
396 | return makePutBodyRequest({ | ||
397 | url, | ||
398 | path, | ||
399 | fields: body, | ||
400 | token: accessToken, | ||
401 | statusCodeExpected | ||
402 | }) | ||
403 | } | ||
404 | |||
405 | function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) { | ||
406 | const path = '/api/v1/videos/' + id + '/rate' | ||
407 | |||
408 | return request(url) | ||
409 | .put(path) | ||
410 | .set('Accept', 'application/json') | ||
411 | .set('Authorization', 'Bearer ' + accessToken) | ||
412 | .send({ rating }) | ||
413 | .expect(specialStatus) | ||
414 | } | ||
415 | |||
416 | function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) { | ||
417 | return new Promise<any>((res, rej) => { | ||
418 | const torrentName = videoUUID + '-' + resolution + '.torrent' | ||
419 | const torrentPath = join(__dirname, '..', '..', '..', '..', 'test' + server.serverNumber, 'torrents', torrentName) | ||
420 | readFile(torrentPath, (err, data) => { | ||
421 | if (err) return rej(err) | ||
422 | |||
423 | return res(parseTorrent(data)) | ||
424 | }) | ||
425 | }) | ||
426 | } | ||
427 | |||
428 | async function completeVideoCheck ( | ||
429 | url: string, | ||
430 | video: any, | ||
431 | attributes: { | ||
432 | name: string | ||
433 | category: number | ||
434 | licence: number | ||
435 | language: string | ||
436 | nsfw: boolean | ||
437 | commentsEnabled: boolean | ||
438 | description: string | ||
439 | publishedAt?: string | ||
440 | support: string | ||
441 | account: { | ||
442 | name: string | ||
443 | host: string | ||
444 | } | ||
445 | isLocal: boolean | ||
446 | tags: string[] | ||
447 | privacy: number | ||
448 | likes?: number | ||
449 | dislikes?: number | ||
450 | duration: number | ||
451 | channel: { | ||
452 | displayName: string | ||
453 | name: string | ||
454 | description | ||
455 | isLocal: boolean | ||
456 | } | ||
457 | fixture: string | ||
458 | files: { | ||
459 | resolution: number | ||
460 | size: number | ||
461 | }[], | ||
462 | thumbnailfile?: string | ||
463 | previewfile?: string | ||
464 | } | ||
465 | ) { | ||
466 | if (!attributes.likes) attributes.likes = 0 | ||
467 | if (!attributes.dislikes) attributes.dislikes = 0 | ||
468 | |||
469 | expect(video.name).to.equal(attributes.name) | ||
470 | expect(video.category.id).to.equal(attributes.category) | ||
471 | expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc') | ||
472 | expect(video.licence.id).to.equal(attributes.licence) | ||
473 | expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown') | ||
474 | expect(video.language.id).to.equal(attributes.language) | ||
475 | expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown') | ||
476 | expect(video.privacy.id).to.deep.equal(attributes.privacy) | ||
477 | expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy]) | ||
478 | expect(video.nsfw).to.equal(attributes.nsfw) | ||
479 | expect(video.description).to.equal(attributes.description) | ||
480 | expect(video.account.id).to.be.a('number') | ||
481 | expect(video.account.uuid).to.be.a('string') | ||
482 | expect(video.account.host).to.equal(attributes.account.host) | ||
483 | expect(video.account.name).to.equal(attributes.account.name) | ||
484 | expect(video.channel.displayName).to.equal(attributes.channel.displayName) | ||
485 | expect(video.channel.name).to.equal(attributes.channel.name) | ||
486 | expect(video.likes).to.equal(attributes.likes) | ||
487 | expect(video.dislikes).to.equal(attributes.dislikes) | ||
488 | expect(video.isLocal).to.equal(attributes.isLocal) | ||
489 | expect(video.duration).to.equal(attributes.duration) | ||
490 | expect(dateIsValid(video.createdAt)).to.be.true | ||
491 | expect(dateIsValid(video.publishedAt)).to.be.true | ||
492 | expect(dateIsValid(video.updatedAt)).to.be.true | ||
493 | |||
494 | if (attributes.publishedAt) { | ||
495 | expect(video.publishedAt).to.equal(attributes.publishedAt) | ||
496 | } | ||
497 | |||
498 | const res = await getVideo(url, video.uuid) | ||
499 | const videoDetails: VideoDetails = res.body | ||
500 | |||
501 | expect(videoDetails.files).to.have.lengthOf(attributes.files.length) | ||
502 | expect(videoDetails.tags).to.deep.equal(attributes.tags) | ||
503 | expect(videoDetails.account.name).to.equal(attributes.account.name) | ||
504 | expect(videoDetails.account.host).to.equal(attributes.account.host) | ||
505 | expect(video.channel.displayName).to.equal(attributes.channel.displayName) | ||
506 | expect(video.channel.name).to.equal(attributes.channel.name) | ||
507 | expect(videoDetails.channel.host).to.equal(attributes.account.host) | ||
508 | expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal) | ||
509 | expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true | ||
510 | expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true | ||
511 | expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled) | ||
512 | |||
513 | for (const attributeFile of attributes.files) { | ||
514 | const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution) | ||
515 | expect(file).not.to.be.undefined | ||
516 | |||
517 | let extension = extname(attributes.fixture) | ||
518 | // Transcoding enabled on server 2, extension will always be .mp4 | ||
519 | if (attributes.account.host === 'localhost:9002') extension = '.mp4' | ||
520 | |||
521 | const magnetUri = file.magnetUri | ||
522 | expect(file.magnetUri).to.have.lengthOf.above(2) | ||
523 | expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`) | ||
524 | expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`) | ||
525 | expect(file.resolution.id).to.equal(attributeFile.resolution) | ||
526 | expect(file.resolution.label).to.equal(attributeFile.resolution + 'p') | ||
527 | |||
528 | const minSize = attributeFile.size - ((10 * attributeFile.size) / 100) | ||
529 | const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100) | ||
530 | expect(file.size, | ||
531 | 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')') | ||
532 | .to.be.above(minSize).and.below(maxSize) | ||
533 | |||
534 | { | ||
535 | await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) | ||
536 | } | ||
537 | |||
538 | if (attributes.previewfile) { | ||
539 | await testImage(url, attributes.previewfile, videoDetails.previewPath) | ||
540 | } | ||
541 | |||
542 | const torrent = await webtorrentAdd(magnetUri, true) | ||
543 | expect(torrent.files).to.be.an('array') | ||
544 | expect(torrent.files.length).to.equal(1) | ||
545 | expect(torrent.files[0].path).to.exist.and.to.not.equal('') | ||
546 | } | ||
547 | } | ||
548 | |||
549 | // --------------------------------------------------------------------------- | ||
550 | |||
551 | export { | ||
552 | getVideoDescription, | ||
553 | getVideoCategories, | ||
554 | getVideoLicences, | ||
555 | getVideoPrivacies, | ||
556 | getVideoLanguages, | ||
557 | getMyVideos, | ||
558 | getAccountVideos, | ||
559 | getVideoChannelVideos, | ||
560 | getVideo, | ||
561 | getVideoWithToken, | ||
562 | getVideosList, | ||
563 | getVideosListPagination, | ||
564 | getVideosListSort, | ||
565 | removeVideo, | ||
566 | getVideosListWithToken, | ||
567 | uploadVideo, | ||
568 | getVideosWithFilters, | ||
569 | updateVideo, | ||
570 | rateVideo, | ||
571 | viewVideo, | ||
572 | parseTorrentVideo, | ||
573 | getLocalVideos, | ||
574 | completeVideoCheck, | ||
575 | checkVideoFilesWereRemoved | ||
576 | } | ||
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 21505b79d..f50aafc35 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts | |||
@@ -6,7 +6,7 @@ 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' |
@@ -58,6 +58,7 @@ getSettings() | |||
58 | settings.remotes[settings.default] : | 58 | settings.remotes[settings.default] : |
59 | settings.remotes[0] | 59 | settings.remotes[0] |
60 | } | 60 | } |
61 | |||
61 | if (!program['username']) program['username'] = netrc.machines[program['url']].login | 62 | if (!program['username']) program['username'] = netrc.machines[program['url']].login |
62 | if (!program['password']) program['password'] = netrc.machines[program['url']].password | 63 | if (!program['password']) program['password'] = netrc.machines[program['url']].password |
63 | } | 64 | } |
@@ -69,12 +70,19 @@ getSettings() | |||
69 | process.exit(-1) | 70 | process.exit(-1) |
70 | } | 71 | } |
71 | 72 | ||
73 | removeEndSlashes(program['url']) | ||
74 | removeEndSlashes(program['targetUrl']) | ||
75 | |||
72 | const user = { | 76 | const user = { |
73 | username: program['username'], | 77 | username: program['username'], |
74 | password: program['password'] | 78 | password: program['password'] |
75 | } | 79 | } |
76 | 80 | ||
77 | 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 | }) | ||
78 | }) | 86 | }) |
79 | 87 | ||
80 | async function promptPassword () { | 88 | async function promptPassword () { |
@@ -108,8 +116,12 @@ async function run (user, url: string) { | |||
108 | secret: res.body.client_secret | 116 | secret: res.body.client_secret |
109 | } | 117 | } |
110 | 118 | ||
111 | const res2 = await login(url, client, user) | 119 | try { |
112 | 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 | } | ||
113 | 125 | ||
114 | const youtubeDL = await safeGetYoutubeDL() | 126 | const youtubeDL = await safeGetYoutubeDL() |
115 | 127 | ||
@@ -321,3 +333,9 @@ function isNSFW (info: any) { | |||
321 | 333 | ||
322 | return false | 334 | return false |
323 | } | 335 | } |
336 | |||
337 | function removeEndSlashes (url: string) { | ||
338 | while (url.endsWith('/')) { | ||
339 | url.slice(0, -1) | ||
340 | } | ||
341 | } | ||
diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts index 6248fb47d..cc7bd9b4c 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 | ||