aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-06 12:26:58 +0100
committerChocobozzz <me@florianbigard.com>2019-02-06 12:26:58 +0100
commit73471b1a52f242e86364ffb077ea6cadb3b07ae2 (patch)
tree43dbb7748e281f8d80f15326f489cdea10ec857d /server
parentc22419dd265c0c7185bf4197a1cb286eb3d8ebc0 (diff)
parentf5305c04aae14467d6f957b713c5a902275cbb89 (diff)
downloadPeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.tar.gz
PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.tar.zst
PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.zip
Merge branch 'release/v1.2.0'
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/accounts.ts11
-rw-r--r--server/controllers/api/config.ts63
-rw-r--r--server/controllers/api/server/contact.ts28
-rw-r--r--server/controllers/api/server/index.ts2
-rw-r--r--server/controllers/api/server/stats.ts7
-rw-r--r--server/controllers/api/users/index.ts9
-rw-r--r--server/controllers/api/users/me.ts163
-rw-r--r--server/controllers/api/users/my-history.ts57
-rw-r--r--server/controllers/api/users/my-notifications.ts108
-rw-r--r--server/controllers/api/users/my-subscriptions.ts170
-rw-r--r--server/controllers/api/video-channel.ts14
-rw-r--r--server/controllers/api/videos/abuse.ts3
-rw-r--r--server/controllers/api/videos/blacklist.ts31
-rw-r--r--server/controllers/api/videos/captions.ts4
-rw-r--r--server/controllers/api/videos/comment.ts3
-rw-r--r--server/controllers/api/videos/import.ts21
-rw-r--r--server/controllers/api/videos/index.ts45
-rw-r--r--server/controllers/bots.ts101
-rw-r--r--server/controllers/client.ts21
-rw-r--r--server/controllers/feeds.ts2
-rw-r--r--server/controllers/index.ts1
-rw-r--r--server/controllers/static.ts15
-rw-r--r--server/controllers/tracker.ts21
-rw-r--r--server/helpers/activitypub.ts4
-rw-r--r--server/helpers/captions-utils.ts4
-rw-r--r--server/helpers/core-utils.ts20
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts99
-rw-r--r--server/helpers/custom-validators/activitypub/actor.ts29
-rw-r--r--server/helpers/custom-validators/activitypub/announce.ts13
-rw-r--r--server/helpers/custom-validators/activitypub/cache-file.ts16
-rw-r--r--server/helpers/custom-validators/activitypub/flag.ts14
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts24
-rw-r--r--server/helpers/custom-validators/activitypub/rate.ts15
-rw-r--r--server/helpers/custom-validators/activitypub/undo.ts20
-rw-r--r--server/helpers/custom-validators/activitypub/video-comments.ts11
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts19
-rw-r--r--server/helpers/custom-validators/activitypub/view.ts10
-rw-r--r--server/helpers/custom-validators/misc.ts5
-rw-r--r--server/helpers/custom-validators/servers.ts11
-rw-r--r--server/helpers/custom-validators/user-notifications.ts23
-rw-r--r--server/helpers/custom-validators/users.ts5
-rw-r--r--server/helpers/custom-validators/video-captions.ts4
-rw-r--r--server/helpers/custom-validators/video-imports.ts4
-rw-r--r--server/helpers/custom-validators/videos.ts13
-rw-r--r--server/helpers/express-utils.ts4
-rw-r--r--server/helpers/ffmpeg-utils.ts10
-rw-r--r--server/helpers/image-utils.ts14
-rw-r--r--server/helpers/regexp.ts23
-rw-r--r--server/helpers/requests.ts9
-rw-r--r--server/helpers/utils.ts12
-rw-r--r--server/helpers/webtorrent.ts6
-rw-r--r--server/helpers/youtube-dl.ts4
-rw-r--r--server/initializers/checker-after-init.ts14
-rw-r--r--server/initializers/checker-before-init.ts5
-rw-r--r--server/initializers/constants.ts134
-rw-r--r--server/initializers/database.ts6
-rw-r--r--server/initializers/migrations/0295-video-file-extname.ts49
-rw-r--r--server/initializers/migrations/0300-user-videos-history-enabled.ts27
-rw-r--r--server/initializers/migrations/0305-fix-unfederated-videos.ts52
-rw-r--r--server/initializers/migrations/0310-drop-unused-video-indexes.ts32
-rw-r--r--server/initializers/migrations/0315-user-notifications.ts47
-rw-r--r--server/initializers/migrations/0320-blacklist-unfederate.ts27
-rw-r--r--server/initializers/migrations/0325-video-abuse-fields.ts37
-rw-r--r--server/lib/activitypub/actor.ts137
-rw-r--r--server/lib/activitypub/process/process-accept.ts1
-rw-r--r--server/lib/activitypub/process/process-announce.ts8
-rw-r--r--server/lib/activitypub/process/process-create.ts125
-rw-r--r--server/lib/activitypub/process/process-dislike.ts52
-rw-r--r--server/lib/activitypub/process/process-flag.ts49
-rw-r--r--server/lib/activitypub/process/process-follow.ts14
-rw-r--r--server/lib/activitypub/process/process-like.ts3
-rw-r--r--server/lib/activitypub/process/process-undo.ts8
-rw-r--r--server/lib/activitypub/process/process-update.ts2
-rw-r--r--server/lib/activitypub/process/process-view.ts35
-rw-r--r--server/lib/activitypub/process/process.ts14
-rw-r--r--server/lib/activitypub/share.ts20
-rw-r--r--server/lib/activitypub/video-comments.ts4
-rw-r--r--server/lib/activitypub/video-rates.ts4
-rw-r--r--server/lib/activitypub/videos.ts54
-rw-r--r--server/lib/cache/actor-follow-score-cache.ts46
-rw-r--r--server/lib/cache/index.ts1
-rw-r--r--server/lib/client-html.ts61
-rw-r--r--server/lib/emailer.ts261
-rw-r--r--server/lib/job-queue/handlers/activitypub-follow.ts9
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-broadcast.ts3
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-unicast.ts6
-rw-r--r--server/lib/job-queue/handlers/activitypub-refresher.ts26
-rw-r--r--server/lib/job-queue/handlers/email.ts3
-rw-r--r--server/lib/job-queue/handlers/video-file.ts47
-rw-r--r--server/lib/job-queue/handlers/video-import.ts23
-rw-r--r--server/lib/job-queue/handlers/video-views.ts4
-rw-r--r--server/lib/job-queue/job-queue.ts5
-rw-r--r--server/lib/notifier.ts455
-rw-r--r--server/lib/oauth-model.ts3
-rw-r--r--server/lib/peertube-socket.ts52
-rw-r--r--server/lib/redis.ts33
-rw-r--r--server/lib/schedulers/abstract-scheduler.ts18
-rw-r--r--server/lib/schedulers/actor-follow-scheduler.ts (renamed from server/lib/schedulers/bad-actor-follow-scheduler.ts)23
-rw-r--r--server/lib/schedulers/remove-old-jobs-scheduler.ts6
-rw-r--r--server/lib/schedulers/update-videos-scheduler.ts32
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts17
-rw-r--r--server/lib/schedulers/youtube-dl-update-scheduler.ts2
-rw-r--r--server/lib/user.ts23
-rw-r--r--server/lib/video-transcoding.ts4
-rw-r--r--server/middlewares/csp.ts44
-rw-r--r--server/middlewares/dnt.ts2
-rw-r--r--server/middlewares/index.ts2
-rw-r--r--server/middlewares/oauth.ts22
-rw-r--r--server/middlewares/validators/config.ts19
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/server.ts49
-rw-r--r--server/middlewares/validators/sort.ts5
-rw-r--r--server/middlewares/validators/user-history.ts26
-rw-r--r--server/middlewares/validators/user-notifications.ts63
-rw-r--r--server/middlewares/validators/users.ts11
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts15
-rw-r--r--server/middlewares/validators/videos/video-watch.ts7
-rw-r--r--server/models/account/account-blocklist.ts31
-rw-r--r--server/models/account/account.ts25
-rw-r--r--server/models/account/user-notification-setting.ts150
-rw-r--r--server/models/account/user-notification.ts472
-rw-r--r--server/models/account/user-video-history.ts33
-rw-r--r--server/models/account/user.ts180
-rw-r--r--server/models/activitypub/actor-follow.ts58
-rw-r--r--server/models/activitypub/actor.ts1
-rw-r--r--server/models/redundancy/video-redundancy.ts10
-rw-r--r--server/models/utils.ts6
-rw-r--r--server/models/video/video-abuse.ts24
-rw-r--r--server/models/video/video-blacklist.ts31
-rw-r--r--server/models/video/video-channel.ts25
-rw-r--r--server/models/video/video-comment.ts45
-rw-r--r--server/models/video/video-file.ts26
-rw-r--r--server/models/video/video-format-utils.ts6
-rw-r--r--server/models/video/video-import.ts4
-rw-r--r--server/models/video/video.ts112
-rw-r--r--server/tests/api/activitypub/client.ts6
-rw-r--r--server/tests/api/activitypub/fetch.ts9
-rw-r--r--server/tests/api/activitypub/helpers.ts10
-rw-r--r--server/tests/api/activitypub/refresher.ts19
-rw-r--r--server/tests/api/activitypub/security.ts12
-rw-r--r--server/tests/api/check-params/accounts.ts14
-rw-r--r--server/tests/api/check-params/blocklist.ts8
-rw-r--r--server/tests/api/check-params/config.ts6
-rw-r--r--server/tests/api/check-params/contact-form.ts96
-rw-r--r--server/tests/api/check-params/follows.ts8
-rw-r--r--server/tests/api/check-params/index.ts3
-rw-r--r--server/tests/api/check-params/jobs.ts18
-rw-r--r--server/tests/api/check-params/redundancy.ts2
-rw-r--r--server/tests/api/check-params/search.ts8
-rw-r--r--server/tests/api/check-params/services.ts10
-rw-r--r--server/tests/api/check-params/user-notifications.ts297
-rw-r--r--server/tests/api/check-params/user-subscriptions.ts11
-rw-r--r--server/tests/api/check-params/users.ts31
-rw-r--r--server/tests/api/check-params/video-abuses.ts14
-rw-r--r--server/tests/api/check-params/video-blacklist.ts126
-rw-r--r--server/tests/api/check-params/video-captions.ts4
-rw-r--r--server/tests/api/check-params/video-channels.ts8
-rw-r--r--server/tests/api/check-params/video-comments.ts10
-rw-r--r--server/tests/api/check-params/video-imports.ts10
-rw-r--r--server/tests/api/check-params/videos-filter.ts2
-rw-r--r--server/tests/api/check-params/videos-history.ts72
-rw-r--r--server/tests/api/check-params/videos.ts17
-rw-r--r--server/tests/api/redundancy/redundancy.ts36
-rw-r--r--server/tests/api/search/search-activitypub-video-channels.ts6
-rw-r--r--server/tests/api/search/search-activitypub-videos.ts4
-rw-r--r--server/tests/api/search/search-videos.ts2
-rw-r--r--server/tests/api/server/config.ts56
-rw-r--r--server/tests/api/server/contact-form.ts86
-rw-r--r--server/tests/api/server/email.ts16
-rw-r--r--server/tests/api/server/follow-constraints.ts20
-rw-r--r--server/tests/api/server/follows.ts27
-rw-r--r--server/tests/api/server/handle-down.ts16
-rw-r--r--server/tests/api/server/index.ts1
-rw-r--r--server/tests/api/server/jobs.ts12
-rw-r--r--server/tests/api/server/no-client.ts4
-rw-r--r--server/tests/api/server/reverse-proxy.ts4
-rw-r--r--server/tests/api/server/stats.ts14
-rw-r--r--server/tests/api/server/tracker.ts4
-rw-r--r--server/tests/api/users/blocklist.ts12
-rw-r--r--server/tests/api/users/index.ts3
-rw-r--r--server/tests/api/users/user-notifications.ts1017
-rw-r--r--server/tests/api/users/user-subscriptions.ts19
-rw-r--r--server/tests/api/users/users-multiple-servers.ts10
-rw-r--r--server/tests/api/users/users-verification.ts13
-rw-r--r--server/tests/api/users/users.ts12
-rw-r--r--server/tests/api/videos/index.ts1
-rw-r--r--server/tests/api/videos/multiple-servers.ts15
-rw-r--r--server/tests/api/videos/services.ts12
-rw-r--r--server/tests/api/videos/single-server.ts2
-rw-r--r--server/tests/api/videos/video-abuse.ts6
-rw-r--r--server/tests/api/videos/video-blacklist-management.ts192
-rw-r--r--server/tests/api/videos/video-blacklist.ts305
-rw-r--r--server/tests/api/videos/video-captions.ts15
-rw-r--r--server/tests/api/videos/video-change-ownership.ts4
-rw-r--r--server/tests/api/videos/video-channels.ts6
-rw-r--r--server/tests/api/videos/video-comments.ts6
-rw-r--r--server/tests/api/videos/video-description.ts6
-rw-r--r--server/tests/api/videos/video-imports.ts6
-rw-r--r--server/tests/api/videos/video-nsfw.ts17
-rw-r--r--server/tests/api/videos/video-privacy.ts12
-rw-r--r--server/tests/api/videos/video-schedule-update.ts4
-rw-r--r--server/tests/api/videos/video-transcoder.ts34
-rw-r--r--server/tests/api/videos/videos-filter.ts2
-rw-r--r--server/tests/api/videos/videos-history.ts87
-rw-r--r--server/tests/api/videos/videos-overview.ts4
-rw-r--r--server/tests/cli/create-import-video-file-job.ts4
-rw-r--r--server/tests/cli/create-transcoding-job.ts4
-rw-r--r--server/tests/cli/index.ts1
-rw-r--r--server/tests/cli/optimize-old-videos.ts4
-rw-r--r--server/tests/cli/peertube.ts2
-rw-r--r--server/tests/cli/reset-password.ts2
-rw-r--r--server/tests/cli/update-host.ts8
-rw-r--r--server/tests/client.ts2
-rw-r--r--server/tests/feeds/feeds.ts6
-rw-r--r--server/tests/fixtures/video_short.avibin0 -> 584656 bytes
-rw-r--r--server/tests/fixtures/video_short.mkvbin0 -> 40642 bytes
-rw-r--r--server/tests/fixtures/video_short_240p.mp4bin0 -> 14082 bytes
-rw-r--r--server/tests/helpers/comment-model.ts25
-rw-r--r--server/tests/helpers/core-utils.ts52
-rw-r--r--server/tests/helpers/index.ts1
-rw-r--r--server/tests/misc-endpoints.ts82
-rw-r--r--server/tests/real-world/populate-database.ts2
-rw-r--r--server/tests/real-world/real-world.ts4
-rw-r--r--server/tests/utils/cli/cli.ts24
-rw-r--r--server/tests/utils/feeds/feeds.ts32
-rw-r--r--server/tests/utils/index.ts19
-rw-r--r--server/tests/utils/miscs/email.ts25
-rw-r--r--server/tests/utils/miscs/miscs.ts101
-rw-r--r--server/tests/utils/miscs/sql.ts38
-rw-r--r--server/tests/utils/miscs/stubs.ts14
-rw-r--r--server/tests/utils/overviews/overviews.ts18
-rw-r--r--server/tests/utils/requests/activitypub.ts43
-rw-r--r--server/tests/utils/requests/check-api-params.ts40
-rw-r--r--server/tests/utils/requests/requests.ts168
-rw-r--r--server/tests/utils/search/video-channels.ts22
-rw-r--r--server/tests/utils/search/videos.ts77
-rw-r--r--server/tests/utils/server/activitypub.ts14
-rw-r--r--server/tests/utils/server/clients.ts19
-rw-r--r--server/tests/utils/server/config.ts135
-rw-r--r--server/tests/utils/server/follows.ts79
-rw-r--r--server/tests/utils/server/jobs.ts78
-rw-r--r--server/tests/utils/server/redundancy.ts17
-rw-r--r--server/tests/utils/server/servers.ts185
-rw-r--r--server/tests/utils/server/stats.ts22
-rw-r--r--server/tests/utils/users/accounts.ts63
-rw-r--r--server/tests/utils/users/blocklist.ts197
-rw-r--r--server/tests/utils/users/login.ts62
-rw-r--r--server/tests/utils/users/user-subscriptions.ts82
-rw-r--r--server/tests/utils/users/users.ts298
-rw-r--r--server/tests/utils/videos/services.ts23
-rw-r--r--server/tests/utils/videos/video-abuses.ts65
-rw-r--r--server/tests/utils/videos/video-blacklist.ts67
-rw-r--r--server/tests/utils/videos/video-captions.ts71
-rw-r--r--server/tests/utils/videos/video-change-ownership.ts54
-rw-r--r--server/tests/utils/videos/video-channels.ts118
-rw-r--r--server/tests/utils/videos/video-comments.ts87
-rw-r--r--server/tests/utils/videos/video-history.ts14
-rw-r--r--server/tests/utils/videos/video-imports.ts51
-rw-r--r--server/tests/utils/videos/videos.ts576
-rw-r--r--server/tools/peertube-get-access-token.ts2
-rw-r--r--server/tools/peertube-import-videos.ts26
-rw-r--r--server/tools/peertube-upload.ts4
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'
14import { VideoModel } from '../../models/video/video' 14import { VideoModel } from '../../models/video/video'
15import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 15import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
16import { VideoChannelModel } from '../../models/video/video-channel' 16import { VideoChannelModel } from '../../models/video/video-channel'
17import { JobQueue } from '../../lib/job-queue'
18import { logger } from '../../helpers/logger'
17 19
18const accountsRouter = express.Router() 20const accountsRouter = express.Router()
19 21
@@ -57,6 +59,11 @@ export {
57function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { 59function 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
75async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 82async 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { omit } from 'lodash' 2import { omit, snakeCase } from 'lodash'
3import { ServerConfig, UserRight } from '../../../shared' 3import { ServerConfig, UserRight } from '../../../shared'
4import { About } from '../../../shared/models/server/about.model' 4import { About } from '../../../shared/models/server/about.model'
5import { CustomConfig } from '../../../shared/models/server/custom-config.model' 5import { CustomConfig } from '../../../shared/models/server/custom-config.model'
@@ -11,6 +11,9 @@ import { ClientHtml } from '../../lib/client-html'
11import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' 11import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
12import { remove, writeJSON } from 'fs-extra' 12import { remove, writeJSON } from 'fs-extra'
13import { getServerCommit } from '../../helpers/utils' 13import { getServerCommit } from '../../helpers/utils'
14import { Emailer } from '../../lib/emailer'
15import { isNumeric } from 'validator'
16import { objectConverter } from '../../helpers/core-utils'
14 17
15const packageJSON = require('../../../../package.json') 18const packageJSON = require('../../../../package.json')
16const configRouter = express.Router() 19const 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
152async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { 166async 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
264function 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 @@
1import * as express from 'express'
2import { asyncMiddleware, contactAdministratorValidator } from '../../../middlewares'
3import { Redis } from '../../../lib/redis'
4import { Emailer } from '../../../lib/emailer'
5import { ContactForm } from '../../../../shared/models/server'
6
7const contactRouter = express.Router()
8
9contactRouter.post('/contact',
10 asyncMiddleware(contactAdministratorValidator),
11 asyncMiddleware(contactAdministrator)
12)
13
14async 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
26export {
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'
3import { statsRouter } from './stats' 3import { statsRouter } from './stats'
4import { serverRedundancyRouter } from './redundancy' 4import { serverRedundancyRouter } from './redundancy'
5import { serverBlocklistRouter } from './server-blocklist' 5import { serverBlocklistRouter } from './server-blocklist'
6import { contactRouter } from './contact'
6 7
7const serverRouter = express.Router() 8const serverRouter = express.Router()
8 9
@@ -10,6 +11,7 @@ serverRouter.use('/', serverFollowsRouter)
10serverRouter.use('/', serverRedundancyRouter) 11serverRouter.use('/', serverRedundancyRouter)
11serverRouter.use('/', statsRouter) 12serverRouter.use('/', statsRouter)
12serverRouter.use('/', serverBlocklistRouter) 13serverRouter.use('/', serverBlocklistRouter)
14serverRouter.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'
8import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' 8import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
9import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../../initializers/constants' 9import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../../initializers/constants'
10import { cacheRoute } from '../../../middlewares/cache' 10import { cacheRoute } from '../../../middlewares/cache'
11import { VideoFileModel } from '../../../models/video/video-file'
11 12
12const statsRouter = express.Router() 13const statsRouter = express.Router()
13 14
@@ -16,11 +17,12 @@ statsRouter.get('/stats',
16 asyncMiddleware(getStats) 17 asyncMiddleware(getStats)
17) 18)
18 19
19async function getStats (req: express.Request, res: express.Response, next: express.NextFunction) { 20async 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
38import { meRouter } from './me' 38import { meRouter } from './me'
39import { deleteUserToken } from '../../../lib/oauth-model' 39import { deleteUserToken } from '../../../lib/oauth-model'
40import { myBlocklistRouter } from './my-blocklist' 40import { myBlocklistRouter } from './my-blocklist'
41import { myVideosHistoryRouter } from './my-history'
42import { myNotificationsRouter } from './my-notifications'
43import { Notifier } from '../../../lib/notifier'
44import { mySubscriptionsRouter } from './my-subscriptions'
41 45
42const auditLogger = auditLoggerFactory('users') 46const auditLogger = auditLoggerFactory('users')
43 47
@@ -54,7 +58,10 @@ const askSendEmailLimiter = new RateLimit({
54}) 58})
55 59
56const usersRouter = express.Router() 60const usersRouter = express.Router()
61usersRouter.use('/', myNotificationsRouter)
62usersRouter.use('/', mySubscriptionsRouter)
57usersRouter.use('/', myBlocklistRouter) 63usersRouter.use('/', myBlocklistRouter)
64usersRouter.use('/', myVideosHistoryRouter)
58usersRouter.use('/', meRouter) 65usersRouter.use('/', meRouter)
59 66
60usersRouter.get('/autocomplete', 67usersRouter.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'
2import 'multer' 2import 'multer'
3import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' 3import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../../initializers' 5import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../../initializers'
6import { sendUpdateActor } from '../../../lib/activitypub/send' 6import { sendUpdateActor } from '../../../lib/activitypub/send'
7import { 7import {
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'
20import { 17import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
21 areSubscriptionsExistValidator,
22 deleteMeValidator,
23 userSubscriptionsSortValidator,
24 videoImportsSortValidator,
25 videosSortValidator
26} from '../../../middlewares/validators'
27import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 18import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
28import { UserModel } from '../../../models/account/user' 19import { UserModel } from '../../../models/account/user'
29import { VideoModel } from '../../../models/video/video' 20import { VideoModel } from '../../../models/video/video'
30import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' 21import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
31import { buildNSFWFilter, createReqFiles } from '../../../helpers/express-utils' 22import { createReqFiles } from '../../../helpers/express-utils'
32import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' 23import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
33import { updateAvatarValidator } from '../../../middlewares/validators/avatar' 24import { updateAvatarValidator } from '../../../middlewares/validators/avatar'
34import { updateActorAvatarFile } from '../../../lib/avatar' 25import { updateActorAvatarFile } from '../../../lib/avatar'
35import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 26import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
36import { VideoImportModel } from '../../../models/video/video-import' 27import { VideoImportModel } from '../../../models/video/video-import'
37import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
38import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
39import { JobQueue } from '../../../lib/job-queue'
40import { logger } from '../../../helpers/logger'
41import { AccountModel } from '../../../models/account/account' 28import { AccountModel } from '../../../models/account/account'
42 29
43const auditLogger = auditLoggerFactory('users-me') 30const auditLogger = auditLoggerFactory('users-me')
44 31
45const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) 32const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
46 33
47const meRouter = express.Router() 34const 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
103meRouter.get('/me/subscriptions/videos',
104 authenticate,
105 paginationValidator,
106 videosSortValidator,
107 setDefaultSort,
108 setDefaultPagination,
109 commonVideosFiltersValidator,
110 asyncMiddleware(getUserSubscriptionVideos)
111)
112
113meRouter.get('/me/subscriptions/exist',
114 authenticate,
115 areSubscriptionsExistValidator,
116 asyncMiddleware(areSubscriptionsExist)
117)
118
119meRouter.get('/me/subscriptions',
120 authenticate,
121 paginationValidator,
122 userSubscriptionsSortValidator,
123 setDefaultSort,
124 setDefaultPagination,
125 asyncMiddleware(getUserSubscriptions)
126)
127
128meRouter.post('/me/subscriptions',
129 authenticate,
130 userSubscriptionAddValidator,
131 asyncMiddleware(addUserSubscription)
132)
133
134meRouter.get('/me/subscriptions/:uri',
135 authenticate,
136 userSubscriptionGetValidator,
137 getUserSubscription
138)
139
140meRouter.delete('/me/subscriptions/:uri',
141 authenticate,
142 userSubscriptionGetValidator,
143 asyncRetryTransactionMiddleware(deleteUserSubscription)
144)
145
146// --------------------------------------------------------------------------- 88// ---------------------------------------------------------------------------
147 89
148export { 90export {
@@ -151,100 +93,6 @@ export {
151 93
152// --------------------------------------------------------------------------- 94// ---------------------------------------------------------------------------
153 95
154async 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
185async 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
201function 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
207async 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
217async 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
226async 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
248async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 96async 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
351async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { 200async 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 @@
1import * as express from 'express'
2import {
3 asyncMiddleware,
4 asyncRetryTransactionMiddleware,
5 authenticate,
6 paginationValidator,
7 setDefaultPagination,
8 userHistoryRemoveValidator
9} from '../../../middlewares'
10import { UserModel } from '../../../models/account/user'
11import { getFormattedObjects } from '../../../helpers/utils'
12import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
13import { sequelizeTypescript } from '../../../initializers'
14
15const myVideosHistoryRouter = express.Router()
16
17myVideosHistoryRouter.get('/me/history/videos',
18 authenticate,
19 paginationValidator,
20 setDefaultPagination,
21 asyncMiddleware(listMyVideosHistory)
22)
23
24myVideosHistoryRouter.post('/me/history/videos/remove',
25 authenticate,
26 userHistoryRemoveValidator,
27 asyncRetryTransactionMiddleware(removeUserHistory)
28)
29
30// ---------------------------------------------------------------------------
31
32export {
33 myVideosHistoryRouter
34}
35
36// ---------------------------------------------------------------------------
37
38async 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
46async 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 @@
1import * as express from 'express'
2import 'multer'
3import {
4 asyncMiddleware,
5 asyncRetryTransactionMiddleware,
6 authenticate,
7 paginationValidator,
8 setDefaultPagination,
9 setDefaultSort,
10 userNotificationsSortValidator
11} from '../../../middlewares'
12import { UserModel } from '../../../models/account/user'
13import { getFormattedObjects } from '../../../helpers/utils'
14import { UserNotificationModel } from '../../../models/account/user-notification'
15import { meRouter } from './me'
16import {
17 listUserNotificationsValidator,
18 markAsReadUserNotificationsValidator,
19 updateNotificationSettingsValidator
20} from '../../../middlewares/validators/user-notifications'
21import { UserNotificationSetting } from '../../../../shared/models/users'
22import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting'
23
24const myNotificationsRouter = express.Router()
25
26meRouter.put('/me/notification-settings',
27 authenticate,
28 updateNotificationSettingsValidator,
29 asyncRetryTransactionMiddleware(updateNotificationSettings)
30)
31
32myNotificationsRouter.get('/me/notifications',
33 authenticate,
34 paginationValidator,
35 userNotificationsSortValidator,
36 setDefaultSort,
37 setDefaultPagination,
38 listUserNotificationsValidator,
39 asyncMiddleware(listUserNotifications)
40)
41
42myNotificationsRouter.post('/me/notifications/read',
43 authenticate,
44 markAsReadUserNotificationsValidator,
45 asyncMiddleware(markAsReadUserNotifications)
46)
47
48myNotificationsRouter.post('/me/notifications/read-all',
49 authenticate,
50 asyncMiddleware(markAsReadAllUserNotifications)
51)
52
53export {
54 myNotificationsRouter
55}
56
57// ---------------------------------------------------------------------------
58
59async 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
86async 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
94async 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
102async 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 @@
1import * as express from 'express'
2import 'multer'
3import { getFormattedObjects } from '../../../helpers/utils'
4import { CONFIG, sequelizeTypescript } from '../../../initializers'
5import {
6 asyncMiddleware,
7 asyncRetryTransactionMiddleware,
8 authenticate,
9 commonVideosFiltersValidator,
10 paginationValidator,
11 setDefaultPagination,
12 setDefaultSort,
13 userSubscriptionAddValidator,
14 userSubscriptionGetValidator
15} from '../../../middlewares'
16import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators'
17import { UserModel } from '../../../models/account/user'
18import { VideoModel } from '../../../models/video/video'
19import { buildNSFWFilter } from '../../../helpers/express-utils'
20import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
21import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
22import { JobQueue } from '../../../lib/job-queue'
23import { logger } from '../../../helpers/logger'
24
25const mySubscriptionsRouter = express.Router()
26
27mySubscriptionsRouter.get('/me/subscriptions/videos',
28 authenticate,
29 paginationValidator,
30 videosSortValidator,
31 setDefaultSort,
32 setDefaultPagination,
33 commonVideosFiltersValidator,
34 asyncMiddleware(getUserSubscriptionVideos)
35)
36
37mySubscriptionsRouter.get('/me/subscriptions/exist',
38 authenticate,
39 areSubscriptionsExistValidator,
40 asyncMiddleware(areSubscriptionsExist)
41)
42
43mySubscriptionsRouter.get('/me/subscriptions',
44 authenticate,
45 paginationValidator,
46 userSubscriptionsSortValidator,
47 setDefaultSort,
48 setDefaultPagination,
49 asyncMiddleware(getUserSubscriptions)
50)
51
52mySubscriptionsRouter.post('/me/subscriptions',
53 authenticate,
54 userSubscriptionAddValidator,
55 asyncMiddleware(addUserSubscription)
56)
57
58mySubscriptionsRouter.get('/me/subscriptions/:uri',
59 authenticate,
60 userSubscriptionGetValidator,
61 getUserSubscription
62)
63
64mySubscriptionsRouter.delete('/me/subscriptions/:uri',
65 authenticate,
66 userSubscriptionGetValidator,
67 asyncRetryTransactionMiddleware(deleteUserSubscription)
68)
69
70// ---------------------------------------------------------------------------
71
72export {
73 mySubscriptionsRouter
74}
75
76// ---------------------------------------------------------------------------
77
78async 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
109async 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
125function 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
131async 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
141async 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
150async 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'
22import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 22import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
23import { setAsyncActorKeys } from '../../lib/activitypub' 23import { setAsyncActorKeys } from '../../lib/activitypub'
24import { AccountModel } from '../../models/account/account' 24import { AccountModel } from '../../models/account/account'
25import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' 25import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers'
26import { logger } from '../../helpers/logger' 26import { logger } from '../../helpers/logger'
27import { VideoModel } from '../../models/video/video' 27import { VideoModel } from '../../models/video/video'
28import { updateAvatarValidator } from '../../middlewares/validators/avatar' 28import { updateAvatarValidator } from '../../middlewares/validators/avatar'
@@ -30,9 +30,10 @@ import { updateActorAvatarFile } from '../../lib/avatar'
30import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' 30import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
31import { resetSequelizeInstance } from '../../helpers/database-utils' 31import { resetSequelizeInstance } from '../../helpers/database-utils'
32import { UserModel } from '../../models/account/user' 32import { UserModel } from '../../models/account/user'
33import { JobQueue } from '../../lib/job-queue'
33 34
34const auditLogger = auditLoggerFactory('channels') 35const auditLogger = auditLoggerFactory('channels')
35const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) 36const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
36 37
37const videoChannelRouter = express.Router() 38const videoChannelRouter = express.Router()
38 39
@@ -197,15 +198,20 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
197async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { 198async 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
203async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 209async 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'
22import { VideoAbuseModel } from '../../../models/video/video-abuse' 22import { VideoAbuseModel } from '../../../models/video/video-abuse'
23import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' 23import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger'
24import { UserModel } from '../../../models/account/user' 24import { UserModel } from '../../../models/account/user'
25import { Notifier } from '../../../lib/notifier'
25 26
26const auditLogger = auditLoggerFactory('abuse') 27const auditLogger = auditLoggerFactory('abuse')
27const abuseVideoRouter = express.Router() 28const 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'
17import { VideoBlacklistModel } from '../../../models/video/video-blacklist' 17import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
18import { sequelizeTypescript } from '../../../initializers' 18import { sequelizeTypescript } from '../../../initializers'
19import { Notifier } from '../../../lib/notifier'
20import { VideoModel } from '../../../models/video/video'
21import { sendCreateVideo, sendDeleteVideo, sendUpdateVideo } from '../../../lib/activitypub/send'
22import { federateVideoIfNeeded } from '../../../lib/activitypub'
19 23
20const blacklistRouter = express.Router() 24const 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
74async function updateVideoBlacklistController (req: express.Request, res: express.Response) { 89async 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
93async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { 107async 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'
2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' 2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
3import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators' 3import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
4import { createReqFiles } from '../../../helpers/express-utils' 4import { createReqFiles } from '../../../helpers/express-utils'
5import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' 5import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../../initializers'
6import { getFormattedObjects } from '../../../helpers/utils' 6import { getFormattedObjects } from '../../../helpers/utils'
7import { VideoCaptionModel } from '../../../models/video/video-caption' 7import { VideoCaptionModel } from '../../../models/video/video-caption'
8import { VideoModel } from '../../../models/video/video' 8import { VideoModel } from '../../../models/video/video'
@@ -12,7 +12,7 @@ import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
12 12
13const reqVideoCaptionAdd = createReqFiles( 13const 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'
26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' 26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
27import { AccountModel } from '../../../models/account/account' 27import { AccountModel } from '../../../models/account/account'
28import { UserModel } from '../../../models/account/user' 28import { UserModel } from '../../../models/account/user'
29import { Notifier } from '../../../lib/notifier'
29 30
30const auditLogger = auditLoggerFactory('comments') 31const auditLogger = auditLoggerFactory('comments')
31const videoCommentRouter = express.Router() 32const 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'
3import 'multer' 3import 'multer'
4import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' 4import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
5import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' 5import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
6import { 6import { 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'
14import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' 7import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl'
15import { createReqFiles } from '../../../helpers/express-utils' 8import { createReqFiles } from '../../../helpers/express-utils'
16import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
@@ -28,18 +21,18 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
28import * as Bluebird from 'bluebird' 21import * as Bluebird from 'bluebird'
29import * as parseTorrent from 'parse-torrent' 22import * as parseTorrent from 'parse-torrent'
30import { getSecureTorrentName } from '../../../helpers/utils' 23import { getSecureTorrentName } from '../../../helpers/utils'
31import { readFile, rename } from 'fs-extra' 24import { readFile, move } from 'fs-extra'
32 25
33const auditLogger = auditLoggerFactory('video-imports') 26const auditLogger = auditLoggerFactory('video-imports')
34const videoImportsRouter = express.Router() 27const videoImportsRouter = express.Router()
35 28
36const reqVideoFileImport = createReqFiles( 29const 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 '../../../
8import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 8import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
9import { 9import {
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'
21import { 20import {
@@ -57,27 +56,28 @@ import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-u
57import { videoCaptionsRouter } from './captions' 56import { videoCaptionsRouter } from './captions'
58import { videoImportsRouter } from './import' 57import { videoImportsRouter } from './import'
59import { resetSequelizeInstance } from '../../../helpers/database-utils' 58import { resetSequelizeInstance } from '../../../helpers/database-utils'
60import { rename } from 'fs-extra' 59import { move } from 'fs-extra'
61import { watchingRouter } from './watching' 60import { watchingRouter } from './watching'
61import { Notifier } from '../../../lib/notifier'
62 62
63const auditLogger = auditLoggerFactory('videos') 63const auditLogger = auditLoggerFactory('videos')
64const videosRouter = express.Router() 64const videosRouter = express.Router()
65 65
66const reqVideoFileAdd = createReqFiles( 66const 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)
75const reqVideoFileUpdate = createReqFiles( 75const 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 @@
1import * as express from 'express'
2import { asyncMiddleware } from '../middlewares'
3import { CONFIG, ROUTE_CACHE_LIFETIME } from '../initializers'
4import * as sitemapModule from 'sitemap'
5import { logger } from '../helpers/logger'
6import { VideoModel } from '../models/video/video'
7import { VideoChannelModel } from '../models/video/video-channel'
8import { AccountModel } from '../models/account/account'
9import { cacheRoute } from '../middlewares/cache'
10import { buildNSFWFilter } from '../helpers/express-utils'
11import { truncate } from 'lodash'
12
13const 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
17botsRouter.use('/sitemap.xml',
18 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP)),
19 asyncMiddleware(getSitemap)
20)
21
22// ---------------------------------------------------------------------------
23
24export {
25 botsRouter
26}
27
28// ---------------------------------------------------------------------------
29
30async 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
53async 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
61async 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
69async 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
94function 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'
2import { join } from 'path' 2import { join } from 'path'
3import { root } from '../helpers/core-utils' 3import { root } from '../helpers/core-utils'
4import { ACCEPT_HEADERS, STATIC_MAX_AGE } from '../initializers' 4import { ACCEPT_HEADERS, STATIC_MAX_AGE } from '../initializers'
5import { asyncMiddleware } from '../middlewares' 5import { asyncMiddleware, embedCSP } from '../middlewares'
6import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '../../shared/models/i18n/i18n' 6import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '../../shared/models/i18n/i18n'
7import { ClientHtml } from '../lib/client-html' 7import { ClientHtml } from '../lib/client-html'
8import { logger } from '../helpers/logger' 8import { 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
19clientsRouter.use('/videos/watch/:id', 19clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage))
20 asyncMiddleware(generateWatchHtmlPage)
21)
22 20
23clientsRouter.use('' + 21clientsRouter.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)
30clientsRouter.use('' + 29clientsRouter.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
91async function generateHTMLPage (req: express.Request, res: express.Response, paramLang?: string) { 90async 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'
6export * from './static' 6export * from './static'
7export * from './webfinger' 7export * from './webfinger'
8export * from './tracker' 8export * from './tracker'
9export * 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
37const videosPhysicalPath = CONFIG.STORAGE.VIDEOS_DIR
38staticRouter.use( 37staticRouter.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)
43staticRouter.use( 42staticRouter.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
48staticRouter.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
139staticRouter.use('/.well-known/change-password',
140 (_, res: express.Response) => {
141 res.redirect('/my-account/settings')
142 }
143)
144
134// --------------------------------------------------------------------------- 145// ---------------------------------------------------------------------------
135 146
136export { 147export {
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'
6import { Server as WebSocketServer } from 'ws' 6import { Server as WebSocketServer } from 'ws'
7import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants' 7import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants'
8import { VideoFileModel } from '../models/video/video-file' 8import { VideoFileModel } from '../models/video/video-file'
9import { parse } from 'url'
9 10
10const TrackerServer = bitTorrentTracker.Server 11const TrackerServer = bitTorrentTracker.Server
11 12
@@ -59,16 +60,26 @@ const onHttpRequest = trackerServer.onHttpRequest.bind(trackerServer)
59trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' })) 60trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' }))
60trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' })) 61trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' }))
61 62
62function createWebsocketServer (app: express.Application) { 63function 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
77export { 88export {
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
109function getAPUrl (activity: string | { id: string }) { 109function 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
124export { 124export {
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'
2import { CONFIG } from '../initializers' 2import { CONFIG } from '../initializers'
3import { VideoCaptionModel } from '../models/video/video-caption' 3import { VideoCaptionModel } from '../models/video/video-caption'
4import * as srt2vtt from 'srt-to-vtt' 4import * as srt2vtt from 'srt-to-vtt'
5import { createReadStream, createWriteStream, remove, rename } from 'fs-extra' 5import { createReadStream, createWriteStream, remove, move } from 'fs-extra'
6 6
7async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) { 7async 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'
11import { URL } from 'url' 11import { URL } from 'url'
12import { truncate } from 'lodash' 12import { truncate } from 'lodash'
13import { exec } from 'child_process' 13import { exec } from 'child_process'
14import { isArray } from './custom-validators/misc'
15
16const 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
15const timeTable = { 34const 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 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { Activity, ActivityType } from '../../../../shared/models/activitypub' 2import { Activity, ActivityType } from '../../../../shared/models/activitypub'
3import { 3import { sanitizeAndCheckActorObject } from './actor'
4 isActorAcceptActivityValid, 4import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc'
5 isActorDeleteActivityValid, 5import { isDislikeActivityValid } from './rate'
6 isActorFollowActivityValid, 6import { sanitizeAndCheckVideoCommentObject } from './video-comments'
7 isActorRejectActivityValid, 7import { sanitizeAndCheckVideoTorrentObject } from './videos'
8 isActorUpdateActivityValid
9} from './actor'
10import { isAnnounceActivityValid } from './announce'
11import { isActivityPubUrlValid } from './misc'
12import { isDislikeActivityValid, isLikeActivityValid } from './rate'
13import { isUndoActivityValid } from './undo'
14import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments'
15import {
16 isVideoFlagValid,
17 isVideoTorrentDeleteActivityValid,
18 sanitizeAndCheckVideoTorrentCreateActivity,
19 sanitizeAndCheckVideoTorrentUpdateActivity
20} from './videos'
21import { isViewActivityValid } from './view' 8import { isViewActivityValid } from './view'
22import { exists } from '../misc' 9import { exists } from '../misc'
23import { isCacheFileCreateActivityValid, isCacheFileUpdateActivityValid } from './cache-file' 10import { isCacheFileObjectValid } from './cache-file'
11import { isFlagActivityValid } from './flag'
24 12
25function isRootActivityValid (activity: any) { 13function 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
52function isActivityValid (activity: any) { 43function isActivityValid (activity: any) {
@@ -66,47 +57,79 @@ export {
66 57
67// --------------------------------------------------------------------------- 58// ---------------------------------------------------------------------------
68 59
60function checkViewActivity (activity: any) {
61 return isBaseActivityValid(activity, 'View') &&
62 isViewActivityValid(activity)
63}
64
65function checkFlagActivity (activity: any) {
66 return isBaseActivityValid(activity, 'Flag') &&
67 isFlagActivityValid(activity)
68}
69
70function checkDislikeActivity (activity: any) {
71 return isBaseActivityValid(activity, 'Dislike') &&
72 isDislikeActivityValid(activity)
73}
74
69function checkCreateActivity (activity: any) { 75function 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
78function checkUpdateActivity (activity: any) { 88function 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
84function checkDeleteActivity (activity: any) { 97function 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
90function checkFollowActivity (activity: any) { 103function checkFollowActivity (activity: any) {
91 return isActorFollowActivityValid(activity) 104 return isBaseActivityValid(activity, 'Follow') &&
105 isObjectValid(activity.object)
92} 106}
93 107
94function checkAcceptActivity (activity: any) { 108function checkAcceptActivity (activity: any) {
95 return isActorAcceptActivityValid(activity) 109 return isBaseActivityValid(activity, 'Accept')
96} 110}
97 111
98function checkRejectActivity (activity: any) { 112function checkRejectActivity (activity: any) {
99 return isActorRejectActivityValid(activity) 113 return isBaseActivityValid(activity, 'Reject')
100} 114}
101 115
102function checkAnnounceActivity (activity: any) { 116function checkAnnounceActivity (activity: any) {
103 return isAnnounceActivityValid(activity) 117 return isBaseActivityValid(activity, 'Announce') &&
118 isObjectValid(activity.object)
104} 119}
105 120
106function checkUndoActivity (activity: any) { 121function 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
110function checkLikeActivity (activity: any) { 132function 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
30const actorNameRegExp = new RegExp('^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_\.]+$') 30const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.]'
31const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`)
31function isActorPreferredUsernameValid (preferredUsername: string) { 32function 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
75function isActorFollowActivityValid (activity: any) { 76function sanitizeAndCheckActorObject (object: any) {
76 return isBaseActivityValid(activity, 'Follow') && 77 normalizeActor(object)
77 isActivityPubUrlValid(activity.object)
78}
79
80function isActorAcceptActivityValid (activity: any) {
81 return isBaseActivityValid(activity, 'Accept')
82}
83
84function isActorRejectActivityValid (activity: any) {
85 return isBaseActivityValid(activity, 'Reject')
86}
87
88function 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
95function normalizeActor (actor: any) { 82function normalizeActor (actor: any) {
@@ -127,6 +114,7 @@ function areValidActorHandles (handles: string[]) {
127 114
128export { 115export {
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 @@
1import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
2
3function isAnnounceActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Announce') &&
5 (
6 isActivityPubUrlValid(activity.object) ||
7 (activity.object && isActivityPubUrlValid(activity.object.id))
8 )
9}
10
11export {
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 @@
1import { isActivityPubUrlValid, isBaseActivityValid } from './misc' 1import { isActivityPubUrlValid } from './misc'
2import { isRemoteVideoUrlValid } from './videos' 2import { isRemoteVideoUrlValid } from './videos'
3import { isDateValid, exists } from '../misc' 3import { exists, isDateValid } from '../misc'
4import { CacheFileObject } from '../../../../shared/models/activitypub/objects' 4import { CacheFileObject } from '../../../../shared/models/activitypub/objects'
5 5
6function isCacheFileCreateActivityValid (activity: any) {
7 return isBaseActivityValid(activity, 'Create') &&
8 isCacheFileObjectValid(activity.object)
9}
10
11function isCacheFileUpdateActivityValid (activity: any) {
12 return isBaseActivityValid(activity, 'Update') &&
13 isCacheFileObjectValid(activity.object)
14}
15
16function isCacheFileObjectValid (object: CacheFileObject) { 6function 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
24export { 14export {
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 @@
1import { isActivityPubUrlValid } from './misc'
2import { isVideoAbuseReasonValid } from '../video-abuses'
3
4function isFlagActivityValid (activity: any) {
5 return activity.type === 'Flag' &&
6 isVideoAbuseReasonValid(activity.content) &&
7 isActivityPubUrlValid(activity.object)
8}
9
10// ---------------------------------------------------------------------------
11
12export {
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 ) && 36function isUrlCollectionValid (collection: any) {
37 return collection === undefined ||
38 (Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t)))
39}
40
41function 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 @@
1import { isActivityPubUrlValid, isBaseActivityValid } from './misc' 1import { isActivityPubUrlValid, isObjectValid } from './misc'
2
3function isLikeActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Like') &&
5 isActivityPubUrlValid(activity.object)
6}
7 2
8function isDislikeActivityValid (activity: any) { 3function 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
17export { 11export {
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 @@
1import { isActorFollowActivityValid } from './actor'
2import { isBaseActivityValid } from './misc'
3import { isDislikeActivityValid, isLikeActivityValid } from './rate'
4import { isAnnounceActivityValid } from './announce'
5import { isCacheFileCreateActivityValid } from './cache-file'
6
7function 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
18export {
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'
3import { exists, isArray, isDateValid } from '../misc' 3import { exists, isArray, isDateValid } from '../misc'
4import { isActivityPubUrlValid, isBaseActivityValid } from './misc' 4import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
5 5
6function isVideoCommentCreateActivityValid (activity: any) {
7 return isBaseActivityValid(activity, 'Create') &&
8 sanitizeAndCheckVideoCommentObject(activity.object)
9}
10
11function sanitizeAndCheckVideoCommentObject (comment: any) { 6function 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
28function isVideoCommentDeleteActivityValid (activity: any) {
29 return isBaseActivityValid(activity, 'Delete')
30}
31
32// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
33 24
34export { 25export {
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
14import { VideoState } from '../../../../shared/models/videos' 14import { VideoState } from '../../../../shared/models/videos'
15import { isVideoAbuseReasonValid } from '../video-abuses' 15import { isVideoAbuseReasonValid } from '../video-abuses'
16 16
17function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
18 return isBaseActivityValid(activity, 'Create') &&
19 sanitizeAndCheckVideoTorrentObject(activity.object)
20}
21
22function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { 17function 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
27function isVideoTorrentDeleteActivityValid (activity: any) {
28 return isBaseActivityValid(activity, 'Delete')
29}
30
31function 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
38function isActivityPubVideoDurationValid (value: string) { 22function 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
105export { 89export {
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 @@
1import { isActivityPubUrlValid, isBaseActivityValid } from './misc' 1import { isActivityPubUrlValid } from './misc'
2 2
3function isViewActivityValid (activity: any) { 3function 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
11export { 11export {
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
12function isNotEmptyIntArray (value: any) {
13 return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0
14}
15
12function isDateValid (value: string) { 16function 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
79export { 83export {
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
4import { isArray, exists } from './misc' 4import { isArray, exists } from './misc'
5import { isTestInstance } from '../core-utils' 5import { isTestInstance } from '../core-utils'
6import { CONSTRAINTS_FIELDS } from '../../initializers'
6 7
7function isHostValid (host: string) { 8function isHostValid (host: string) {
8 const isURLOptions = { 9 const isURLOptions = {
@@ -26,9 +27,19 @@ function isEachUniqueHostValid (hosts: string[]) {
26 }) 27 })
27} 28}
28 29
30function isValidContactBody (value: any) {
31 return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.BODY)
32}
33
34function isValidContactFromName (value: any) {
35 return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.FROM_NAME)
36}
37
29// --------------------------------------------------------------------------- 38// ---------------------------------------------------------------------------
30 39
31export { 40export {
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 @@
1import { exists } from './misc'
2import * as validator from 'validator'
3import { UserNotificationType } from '../../../shared/models/users'
4import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
5
6function isUserNotificationTypeValid (value: any) {
7 return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined
8}
9
10function 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
20export {
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
49function isUserVideosHistoryEnabledValid (value: any) {
50 return isBooleanValid(value)
51}
52
49function isUserAutoPlayVideoValid (value: any) { 53function 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
75export { 79export {
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 @@
1import { CONSTRAINTS_FIELDS, VIDEO_CAPTIONS_MIMETYPE_EXT, VIDEO_LANGUAGES } from '../../initializers' 1import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers'
2import { exists, isFileValid } from './misc' 2import { exists, isFileValid } from './misc'
3import { Response } from 'express' 3import { Response } from 'express'
4import { VideoModel } from '../../models/video/video' 4import { 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
11const videoCaptionTypes = Object.keys(VIDEO_CAPTIONS_MIMETYPE_EXT) 11const 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})`)
14const videoCaptionTypesRegex = videoCaptionTypes.join('|') 14const 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 @@
1import 'express-validator' 1import 'express-validator'
2import 'multer' 2import 'multer'
3import * as validator from 'validator' 3import * as validator from 'validator'
4import { CONSTRAINTS_FIELDS, TORRENT_MIMETYPE_EXT, VIDEO_IMPORT_STATES } from '../../initializers' 4import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers'
5import { exists, isFileValid } from './misc' 5import { exists, isFileValid } from './misc'
6import * as express from 'express' 6import * as express from 'express'
7import { VideoImportModel } from '../../models/video/video-import' 7import { 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
27const videoTorrentImportTypes = Object.keys(TORRENT_MIMETYPE_EXT).map(m => `(${m})`) 27const videoTorrentImportTypes = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT).map(m => `(${m})`)
28const videoTorrentImportRegex = videoTorrentImportTypes.join('|') 28const videoTorrentImportRegex = videoTorrentImportTypes.join('|')
29function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 29function 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'
5import * as validator from 'validator' 5import * as validator from 'validator'
6import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' 6import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
7import { 7import {
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
86const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) 85function isVideoFileExtnameValid (value: string) {
87const videoFileTypesRegex = videoFileTypes.join('|') 86 return exists(value) && MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined
87}
88 88
89function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 89function 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'
7import { isArray } from './custom-validators/misc' 7import { isArray } from './custom-validators/misc'
8import { UserModel } from '../models/account/user' 8import { UserModel } from '../models/account/user'
9 9
10function buildNSFWFilter (res: express.Response, paramNSFW?: string) { 10function 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) {
41async function getVideoFileFPS (path: string) { 41async 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 @@
1import 'multer' 1import 'multer'
2import * as sharp from 'sharp' 2import * as sharp from 'sharp'
3import { move, remove } from 'fs-extra' 3import { readFile, remove } from 'fs-extra'
4import { logger } from './logger'
4 5
5async function processImage ( 6async 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
2function 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
21export {
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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { createWriteStream } from 'fs-extra' 2import { createWriteStream } from 'fs-extra'
3import * as request from 'request' 3import * as request from 'request'
4import { ACTIVITY_PUB } from '../initializers' 4import { ACTIVITY_PUB, CONFIG } from '../initializers'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { join } from 'path'
6 7
7function doRequest <T> ( 8function 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
31async function downloadImage (url: string, destPath: string, size: { width: number, height: number }) { 32async 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'
7import { Instance as ParseTorrent } from 'parse-torrent' 7import { Instance as ParseTorrent } from 'parse-torrent'
8import { remove } from 'fs-extra' 8import { remove } from 'fs-extra'
9import * as memoizee from 'memoizee' 9import * as memoizee from 'memoizee'
10import { isArray } from './custom-validators/misc'
10 11
11function deleteFileAsync (path: string) { 12function 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
22interface FormattableToJSON { 23interface FormattableToJSON { toFormattedJSON (args?: any) }
23 toFormattedJSON (args?: any)
24}
25
26function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) { 24function 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
49function generateVideoTmpPath (target: string | ParseTorrent) { 47function 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
56function getSecureTorrentName (originalName: string) { 54function 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 @@
1import { logger } from './logger' 1import { logger } from './logger'
2import { generateVideoTmpPath } from './utils' 2import { generateVideoImportTmpPath } from './utils'
3import * as WebTorrent from 'webtorrent' 3import * as WebTorrent from 'webtorrent'
4import { createWriteStream, ensureDir, remove } from 'fs-extra' 4import { createWriteStream, ensureDir, remove } from 'fs-extra'
5import { CONFIG } from '../initializers' 5import { 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 @@
1import { truncate } from 'lodash' 1import { truncate } from 'lodash'
2import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' 2import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers'
3import { logger } from './logger' 3import { logger } from './logger'
4import { generateVideoTmpPath } from './utils' 4import { generateVideoImportTmpPath } from './utils'
5import { join } from 'path' 5import { join } from 'path'
6import { root } from './core-utils' 6import { root } from './core-utils'
7import { ensureDir, writeFile, remove } from 'fs-extra' 7import { ensureDir, writeFile, remove } from 'fs-extra'
@@ -40,7 +40,7 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo>
40} 40}
41 41
42function downloadYoutubeDLVideo (url: string, timeout: number) { 42function 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'
10import { RecentlyAddedStrategy } from '../../shared/models/redundancy' 10import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
11import { isArray } from '../helpers/custom-validators/misc' 11import { isArray } from '../helpers/custom-validators/misc'
12import { uniq } from 'lodash' 12import { uniq } from 'lodash'
13import { Emailer } from '../lib/emailer'
13 14
14async function checkActivityPubUrls () { 15async 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
34function checkConfig () { 35function 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
19const LAST_MIGRATION_VERSION = 290 19const 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
56const OAUTH_LIFETIME = { 58const OAUTH_LIFETIME = {
@@ -61,6 +63,7 @@ const OAUTH_LIFETIME = {
61const ROUTE_CACHE_LIFETIME = { 63const 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
145let SCHEDULER_INTERVALS_MS = { 148let 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
298const CONSTRAINTS_FIELDS = { 308let 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
404let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour 418let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
419let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
420
405const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { 421const 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
480const VIDEO_MIMETYPE_EXT = { 496const 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 },
485const VIDEO_EXT_MIMETYPE = invert(VIDEO_MIMETYPE_EXT) 501 IMAGE: {
486 502 MIMETYPE_EXT: {
487const 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: {
493const 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 },
498const TORRENT_MIMETYPE_EXT = { 514 TORRENT: {
499 'application/x-bittorrent': '.torrent' 515 MIMETYPE_EXT: {
516 'application/x-bittorrent': '.torrent'
517 }
518 }
500} 519}
520MIMETYPES.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
684updateWebserverConfig() 706updateWebserverUrls()
685 707
686// --------------------------------------------------------------------------- 708// ---------------------------------------------------------------------------
687 709
688export { 710export {
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
767function updateWebserverConfig () { 786function 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
807function 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
812function 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
819function 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
772function buildVideosRedundancy (objs: any[]): VideosRedundancy[] { 825function 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'
31import { UserVideoHistoryModel } from '../models/account/user-video-history' 31import { UserVideoHistoryModel } from '../models/account/user-video-history'
32import { AccountBlocklistModel } from '../models/account/account-blocklist' 32import { AccountBlocklistModel } from '../models/account/account-blocklist'
33import { ServerBlocklistModel } from '../models/server/server-blocklist' 33import { ServerBlocklistModel } from '../models/server/server-blocklist'
34import { UserNotificationModel } from '../models/account/user-notification'
35import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
34 36
35require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 37require('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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
42function down (options) {
43 throw new Error('Not implemented.')
44}
45
46export {
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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
45function down (options) {
46 throw new Error('Not implemented.')
47}
48
49export {
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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
25function down (options) {
26 throw new Error('Not implemented.')
27}
28
29export {
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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8
9 {
10 const query = `
11CREATE 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,
24PRIMARY 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
40function down (options) {
41 throw new Error('Not implemented.')
42}
43
44export {
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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
30function down (options) {
31 throw new Error('Not implemented.')
32}
33
34export {
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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { join } from 'path'
3import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
4import * as url from 'url' 3import * as url from 'url'
5import * as uuidv4 from 'uuid/v4' 4import * as uuidv4 from 'uuid/v4'
6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' 7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
9import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' 8import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 10import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
@@ -13,7 +12,7 @@ import { logger } from '../../helpers/logger'
13import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
14import { doRequest, downloadImage } from '../../helpers/requests' 13import { doRequest, downloadImage } from '../../helpers/requests'
15import { getUrlFromWebfinger } from '../../helpers/webfinger' 14import { getUrlFromWebfinger } from '../../helpers/webfinger'
16import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' 15import { AVATARS_SIZE, CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers'
17import { AccountModel } from '../../models/account/account' 16import { AccountModel } from '../../models/account/account'
18import { ActorModel } from '../../models/activitypub/actor' 17import { ActorModel } from '../../models/activitypub/actor'
19import { AvatarModel } from '../../models/avatar/avatar' 18import { 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
173async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { 172async 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
204async 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
207export { 267export {
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
379async 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'
5import { VideoShareModel } from '../../../models/video/video-share' 5import { VideoShareModel } from '../../../models/video/video-share'
6import { forwardVideoRelatedActivity } from '../send/utils' 6import { forwardVideoRelatedActivity } from '../send/utils'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { VideoPrivacy } from '../../../../shared/models/videos'
9import { Notifier } from '../../notifier'
8 10
9async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) { 11async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) {
10 return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity) 12 return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity)
@@ -21,9 +23,9 @@ export {
21async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) { 23async 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 @@
1import { ActivityCreate, CacheFileObject, VideoAbuseState, VideoTorrentObject } from '../../../../shared' 1import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared'
2import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects'
3import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' 2import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object'
4import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
5import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
6import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers'
7import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
8import { ActorModel } from '../../../models/activitypub/actor' 6import { ActorModel } from '../../../models/activitypub/actor'
9import { VideoAbuseModel } from '../../../models/video/video-abuse'
10import { addVideoComment, resolveThread } from '../video-comments' 7import { addVideoComment, resolveThread } from '../video-comments'
11import { getOrCreateVideoAndAccountAndChannel } from '../videos' 8import { getOrCreateVideoAndAccountAndChannel } from '../videos'
12import { forwardVideoRelatedActivity } from '../send/utils' 9import { forwardVideoRelatedActivity } from '../send/utils'
13import { Redis } from '../../redis'
14import { createOrUpdateCacheFile } from '../cache-file' 10import { createOrUpdateCacheFile } from '../cache-file'
15import { getVideoDislikeActivityPubUrl } from '../url' 11import { Notifier } from '../../notifier'
16import { VideoModel } from '../../../models/video/video' 12import { processViewActivity } from './process-view'
13import { processDislikeActivity } from './process-dislike'
14import { processFlagActivity } from './process-flag'
17 15
18async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { 16async 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 {
48async function processCreateVideo (activity: ActivityCreate) { 56async 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
56async 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
87async 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
105async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { 66async 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
121async function processCreateVideoAbuse (byActor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) { 82async 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
143async 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 @@
1import { ActivityCreate, ActivityDislike } from '../../../../shared'
2import { DislikeObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { sequelizeTypescript } from '../../../initializers'
5import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
6import { ActorModel } from '../../../models/activitypub/actor'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { forwardVideoRelatedActivity } from '../send/utils'
9import { getVideoDislikeActivityPubUrl } from '../url'
10
11async function processDislikeActivity (activity: ActivityCreate | ActivityDislike, byActor: ActorModel) {
12 return retryTransactionWrapper(processDislike, activity, byActor)
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 processDislikeActivity
19}
20
21// ---------------------------------------------------------------------------
22
23async 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 @@
1import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../shared'
2import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers'
6import { ActorModel } from '../../../models/activitypub/actor'
7import { VideoAbuseModel } from '../../../models/video/video-abuse'
8import { getOrCreateVideoAndAccountAndChannel } from '../videos'
9import { Notifier } from '../../notifier'
10import { getAPId } from '../../../helpers/activitypub'
11
12async function processFlagActivity (activity: ActivityCreate | ActivityFlag, byActor: ActorModel) {
13 return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor)
14}
15
16// ---------------------------------------------------------------------------
17
18export {
19 processFlagActivity
20}
21
22// ---------------------------------------------------------------------------
23
24async 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'
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 6import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
7import { sendAccept } from '../send' 7import { sendAccept } from '../send'
8import { Notifier } from '../../notifier'
9import { getAPId } from '../../../helpers/activitypub'
8 10
9async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) { 11async 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
23async function processFollow (actor: ActorModel, targetActorURL: string) { 25async 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'
6import { forwardVideoRelatedActivity } from '../send/utils' 6import { forwardVideoRelatedActivity } from '../send/utils'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { getVideoLikeActivityPubUrl } from '../url' 8import { getVideoLikeActivityPubUrl } from '../url'
9import { getAPId } from '../../../helpers/activitypub'
9 10
10async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { 11async 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
22async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { 23async 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
74async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { 78async 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 @@
1import { ActorModel } from '../../../models/activitypub/actor'
2import { getOrCreateVideoAndAccountAndChannel } from '../videos'
3import { forwardVideoRelatedActivity } from '../send/utils'
4import { Redis } from '../../redis'
5import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub'
6
7async function processViewActivity (activity: ActivityView | ActivityCreate, byActor: ActorModel) {
8 return processCreateView(activity, byActor)
9}
10
11// ---------------------------------------------------------------------------
12
13export {
14 processViewActivity
15}
16
17// ---------------------------------------------------------------------------
18
19async 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 @@
1import { Activity, ActivityType } from '../../../../shared/models/activitypub' 1import { Activity, ActivityType } from '../../../../shared/models/activitypub'
2import { checkUrlsSameHost, getAPUrl } from '../../../helpers/activitypub' 2import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { ActorModel } from '../../../models/activitypub/actor' 4import { ActorModel } from '../../../models/activitypub/actor'
5import { processAcceptActivity } from './process-accept' 5import { processAcceptActivity } from './process-accept'
@@ -12,6 +12,9 @@ import { processRejectActivity } from './process-reject'
12import { processUndoActivity } from './process-undo' 12import { processUndoActivity } from './process-undo'
13import { processUpdateActivity } from './process-update' 13import { processUpdateActivity } from './process-update'
14import { getOrCreateActorAndServerAndModel } from '../actor' 14import { getOrCreateActorAndServerAndModel } from '../actor'
15import { processDislikeActivity } from './process-dislike'
16import { processFlagActivity } from './process-flag'
17import { processViewActivity } from './process-view'
15 18
16const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: ActorModel, inboxActor?: ActorModel) => Promise<any> } = { 19const 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
28async function processActivities ( 34async 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'
11import { getOrCreateActorAndServerAndModel } from './actor' 11import { getOrCreateActorAndServerAndModel } from './actor'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../helpers/logger'
13import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' 13import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
14import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' 14import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
15 15
16async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { 16async 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
98async function shareByVideoChannel (video: VideoModel, t: Transaction) { 96async 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
117async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { 113async 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'
9import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' 10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
11import { doRequest } from '../../helpers/requests' 11import { doRequest } from '../../helpers/requests'
12import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' 12import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
13import { ActorModel } from '../../models/activitypub/actor' 13import { ActorModel } from '../../models/activitypub/actor'
14import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url' 14import { 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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as sequelize from 'sequelize' 2import * as sequelize from 'sequelize'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { join } from 'path'
5import * as request from 'request' 4import * as request from 'request'
6import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' 5import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index'
7import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 6import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
@@ -11,7 +10,7 @@ import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos
11import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 10import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
12import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
13import { doRequest, downloadImage } from '../../helpers/requests' 12import { doRequest, downloadImage } from '../../helpers/requests'
14import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers' 13import { ACTIVITY_PUB, CONFIG, MIMETYPES, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers'
15import { ActorModel } from '../../models/activitypub/actor' 14import { ActorModel } from '../../models/activitypub/actor'
16import { TagModel } from '../../models/video/tag' 15import { TagModel } from '../../models/video/tag'
17import { VideoModel } from '../../models/video/video' 16import { VideoModel } from '../../models/video/video'
@@ -29,7 +28,8 @@ import { createRates } from './video-rates'
29import { addVideoShares, shareVideoByServerAndChannel } from './share' 28import { addVideoShares, shareVideoByServerAndChannel } from './share'
30import { AccountModel } from '../../models/account/account' 29import { AccountModel } from '../../models/account/account'
31import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 30import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
32import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' 31import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
32import { Notifier } from '../notifier'
33 33
34async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 34async 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
96function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { 96function 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
103function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 102function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
@@ -156,29 +155,34 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid
156} 155}
157 156
158async function getOrCreateVideoAndAccountAndChannel (options: { 157async 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
195async function updateVideoFromAP (options: { 199async 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
360function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { 370function 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 @@
1import { ACTOR_FOLLOW_SCORE } from '../../initializers'
2import { 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
6class 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
44export {
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 @@
1export * from './actor-follow-score-cache'
1export * from './videos-preview-cache' 2export * from './videos-preview-cache'
2export * from './videos-caption-cache' 3export * 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Bluebird from 'bluebird' 2import * as Bluebird from 'bluebird'
3import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' 3import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n'
4import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, STATIC_PATHS } from '../initializers' 4import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE } from '../initializers'
5import { join } from 'path' 5import { join } from 'path'
6import { escapeHTML } from '../helpers/core-utils' 6import { escapeHTML } from '../helpers/core-utils'
7import { VideoModel } from '../models/video/video' 7import { 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 @@
1import { createTransport, Transporter } from 'nodemailer' 1import { createTransport, Transporter } from 'nodemailer'
2import { UserRight } from '../../shared/models/users'
3import { isTestInstance } from '../helpers/core-utils' 2import { isTestInstance } from '../helpers/core-utils'
4import { bunyanLogger, logger } from '../helpers/logger' 3import { bunyanLogger, logger } from '../helpers/logger'
5import { CONFIG } from '../initializers' 4import { CONFIG } from '../initializers'
@@ -8,6 +7,11 @@ import { VideoModel } from '../models/video/video'
8import { JobQueue } from './job-queue' 7import { JobQueue } from './job-queue'
9import { EmailPayload } from './job-queue/handlers/email' 8import { EmailPayload } from './job-queue/handlers/email'
10import { readFileSync } from 'fs-extra' 9import { readFileSync } from 'fs-extra'
10import { VideoCommentModel } from '../models/video/video-comment'
11import { VideoAbuseModel } from '../models/video/video-abuse'
12import { VideoBlacklistModel } from '../models/video/video-blacklist'
13import { VideoImportModel } from '../models/video/video-import'
14import { ActorFollowModel } from '../models/activitypub/actor-follow'
11 15
12class Emailer { 16class 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'
8import { retryTransactionWrapper } from '../../../helpers/database-utils' 8import { retryTransactionWrapper } from '../../../helpers/database-utils'
9import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 9import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
10import { ActorModel } from '../../../models/activitypub/actor' 10import { ActorModel } from '../../../models/activitypub/actor'
11import { Notifier } from '../../notifier'
11 12
12export type ActivitypubFollowPayload = { 13export type ActivitypubFollowPayload = {
13 followerActorId: number 14 followerActorId: number
@@ -42,7 +43,7 @@ export {
42 43
43// --------------------------------------------------------------------------- 44// ---------------------------------------------------------------------------
44 45
45function follow (fromActor: ActorModel, targetActor: ActorModel) { 46async 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'
5import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 5import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
6import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' 6import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils'
7import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers' 7import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers'
8import { ActorFollowScoreCache } from '../../cache'
8 9
9export type ActivitypubHttpBroadcastPayload = { 10export 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 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { doRequest } from '../../../helpers/requests' 3import { doRequest } from '../../../helpers/requests'
4import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
5import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' 4import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils'
6import { JOB_REQUEST_TIMEOUT } from '../../../initializers' 5import { JOB_REQUEST_TIMEOUT } from '../../../initializers'
6import { ActorFollowScoreCache } from '../../cache'
7 7
8export type ActivitypubHttpUnicastPayload = { 8export 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 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { fetchVideoByUrl } from '../../../helpers/video' 3import { fetchVideoByUrl } from '../../../helpers/video'
4import { refreshVideoIfNeeded } from '../../activitypub' 4import { refreshVideoIfNeeded, refreshActorIfNeeded } from '../../activitypub'
5import { ActorModel } from '../../../models/activitypub/actor'
5 6
6export type RefreshPayload = { 7export type RefreshPayload = {
7 videoUrl: string 8 type: 'video' | 'actor'
8 type: 'video' 9 url: string
9} 10}
10 11
11async function refreshAPObject (job: Bull.Job) { 12async 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
20export { 23export {
24 refreshActor,
21 refreshAPObject 25 refreshAPObject
22} 26}
23 27
24// --------------------------------------------------------------------------- 28// ---------------------------------------------------------------------------
25 29
26async function refreshAPVideo (videoUrl: string) { 30async 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
46async 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
11async function processEmail (job: Bull.Job) { 12async 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'
8import { sequelizeTypescript } from '../../../initializers' 8import { sequelizeTypescript } from '../../../initializers'
9import * as Bluebird from 'bluebird' 9import * as Bluebird from 'bluebird'
10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding' 11import { importVideoFile, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding'
12import { Notifier } from '../../notifier'
12 13
13export type VideoFilePayload = { 14export type VideoFilePayload = {
14 videoUUID: string 15 videoUUID: string
@@ -67,17 +68,17 @@ async function processVideoFile (job: Bull.Job) {
67async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { 68async 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
94async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) { 100async 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
7import { extname, join } from 'path' 7import { extname, join } from 'path'
8import { VideoFileModel } from '../../../models/video/video-file' 8import { VideoFileModel } from '../../../models/video/video-file'
9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' 9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
10import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests' 10import { downloadImage } from '../../../helpers/requests'
11import { VideoState } from '../../../../shared' 11import { VideoState } from '../../../../shared'
12import { JobQueue } from '../index' 12import { JobQueue } from '../index'
13import { federateVideoIfNeeded } from '../../activitypub' 13import { federateVideoIfNeeded } from '../../activitypub'
14import { VideoModel } from '../../../models/video/video' 14import { VideoModel } from '../../../models/video/video'
15import { downloadWebTorrentVideo } from '../../../helpers/webtorrent' 15import { downloadWebTorrentVideo } from '../../../helpers/webtorrent'
16import { getSecureTorrentName } from '../../../helpers/utils' 16import { getSecureTorrentName } from '../../../helpers/utils'
17import { remove, rename, stat } from 'fs-extra' 17import { remove, move, stat } from 'fs-extra'
18import { Notifier } from '../../notifier'
18 19
19type VideoImportYoutubeDLPayload = { 20type 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 @@
1import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
2import { logger } from '../helpers/logger'
3import { VideoModel } from '../models/video/video'
4import { Emailer } from './emailer'
5import { UserNotificationModel } from '../models/account/user-notification'
6import { VideoCommentModel } from '../models/video/video-comment'
7import { UserModel } from '../models/account/user'
8import { PeerTubeSocket } from './peertube-socket'
9import { CONFIG } from '../initializers/constants'
10import { VideoPrivacy, VideoState } from '../../shared/models/videos'
11import { VideoAbuseModel } from '../models/video/video-abuse'
12import { VideoBlacklistModel } from '../models/video/video-blacklist'
13import * as Bluebird from 'bluebird'
14import { VideoImportModel } from '../models/video/video-import'
15import { AccountBlocklistModel } from '../models/account/account-blocklist'
16import { ActorFollowModel } from '../models/activitypub/actor-follow'
17import { AccountModel } from '../models/account/account'
18
19class 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
453export {
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 @@
1import * as Bluebird from 'bluebird'
1import { AccessDeniedError } from 'oauth2-server' 2import { AccessDeniedError } from 'oauth2-server'
2import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
3import { UserModel } from '../models/account/user' 4import { UserModel } from '../models/account/user'
@@ -37,7 +38,7 @@ function clearCacheByToken (token: string) {
37function getAccessToken (bearerToken: string) { 38function 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 @@
1import * as SocketIO from 'socket.io'
2import { authenticateSocket } from '../middlewares'
3import { UserNotificationModel } from '../models/account/user-notification'
4import { logger } from '../helpers/logger'
5import { Server } from 'http'
6
7class 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
50export {
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'
2import { createClient, RedisClient } from 'redis' 2import { createClient, RedisClient } from 'redis'
3import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
4import { generateRandomString } from '../helpers/utils' 4import { generateRandomString } from '../helpers/utils'
5import { CONFIG, USER_PASSWORD_RESET_LIFETIME, USER_EMAIL_VERIFY_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers' 5import {
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
7type CachedRoute = { 13type 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 @@
1import { logger } from '../../helpers/logger'
2
1export abstract class AbstractScheduler { 3export 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'
3import { ActorFollowModel } from '../../models/activitypub/actor-follow' 3import { ActorFollowModel } from '../../models/activitypub/actor-follow'
4import { AbstractScheduler } from './abstract-scheduler' 4import { AbstractScheduler } from './abstract-scheduler'
5import { SCHEDULER_INTERVALS_MS } from '../../initializers' 5import { SCHEDULER_INTERVALS_MS } from '../../initializers'
6import { ActorFollowScoreCache } from '../cache'
6 7
7export class BadActorFollowScheduler extends AbstractScheduler { 8export 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'
5import { federateVideoIfNeeded } from '../activitypub' 5import { federateVideoIfNeeded } from '../activitypub'
6import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers' 6import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers'
7import { VideoPrivacy } from '../../../shared/models/videos' 7import { VideoPrivacy } from '../../../shared/models/videos'
8import { Notifier } from '../notifier'
9import { VideoModel } from '../../models/video/video'
8 10
9export class UpdateVideosScheduler extends AbstractScheduler { 11export 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'
6import { VideoFileModel } from '../../models/video/video-file' 6import { VideoFileModel } from '../../models/video/video-file'
7import { downloadWebTorrentVideo } from '../../helpers/webtorrent' 7import { downloadWebTorrentVideo } from '../../helpers/webtorrent'
8import { join } from 'path' 8import { join } from 'path'
9import { rename } from 'fs-extra' 9import { move } from 'fs-extra'
10import { getServerActor } from '../../helpers/utils' 10import { getServerActor } from '../../helpers/utils'
11import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' 11import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send'
12import { getVideoCacheFileActivityPubUrl } from '../activitypub/url' 12import { getVideoCacheFileActivityPubUrl } from '../activitypub/url'
@@ -16,7 +16,6 @@ import { getOrCreateVideoAndAccountAndChannel } from '../activitypub'
16export class VideosRedundancyScheduler extends AbstractScheduler { 16export 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'
9import { VideoChannelModel } from '../models/video/video-channel' 9import { VideoChannelModel } from '../models/video/video-channel'
10import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' 10import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
11import { ActorModel } from '../models/activitypub/actor' 11import { ActorModel } from '../models/activitypub/actor'
12import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
13import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
12 14
13async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) { 15async 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
98function 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 @@
1import { CONFIG } from '../initializers' 1import { CONFIG } from '../initializers'
2import { extname, join } from 'path' 2import { extname, join } from 'path'
3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' 3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils'
4import { copy, remove, rename, stat } from 'fs-extra' 4import { copy, remove, move, stat } from 'fs-extra'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { VideoResolution } from '../../shared/models/videos' 6import { VideoResolution } from '../../shared/models/videos'
7import { VideoFileModel } from '../models/video/video-file' 7import { 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 @@
1import * as helmet from 'helmet'
2import { CONFIG } from '../initializers/constants'
3
4const 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
25const baseCSP = helmet.contentSecurityPolicy({
26 directives: baseDirectives,
27 browserSniff: false,
28 reportOnly: true
29})
30
31const 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
41export {
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
11export { 11export {
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'
6export * from './servers' 6export * from './servers'
7export * from './sort' 7export * from './sort'
8export * from './user-right' 8export * from './user-right'
9export * from './dnt'
10export * 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'
3import 'express-validator' 3import 'express-validator'
4import { OAUTH_LIFETIME } from '../initializers' 4import { OAUTH_LIFETIME } from '../initializers'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { Socket } from 'socket.io'
7import { getAccessToken } from '../lib/oauth-model'
6 8
7const oAuthServer = new OAuthServer({ 9const 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
33function 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
31function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) { 52function 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
69export { 90export {
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body } from 'express-validator/check' 2import { body } from 'express-validator/check'
3import { isUserNSFWPolicyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' 3import { isUserNSFWPolicyValid, isUserVideoQuotaValid, isUserVideoQuotaDailyValid } from '../../helpers/custom-validators/users'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6 6
7const customConfigUpdateValidator = [ 7const 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'
12export * from './webfinger' 12export * from './webfinger'
13export * from './search' 13export * from './search'
14export * from './server' 14export * from './server'
15export * 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { logger } from '../../helpers/logger' 2import { logger } from '../../helpers/logger'
3import { areValidationErrors } from './utils' 3import { areValidationErrors } from './utils'
4import { isHostValid } from '../../helpers/custom-validators/servers' 4import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers'
5import { ServerModel } from '../../models/server/server' 5import { ServerModel } from '../../models/server/server'
6import { body } from 'express-validator/check' 6import { body } from 'express-validator/check'
7import { isUserDisplayNameValid } from '../../helpers/custom-validators/users'
8import { Emailer } from '../../lib/emailer'
9import { Redis } from '../../lib/redis'
10import { CONFIG } from '../../initializers/constants'
7 11
8const serverGetValidator = [ 12const 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
33const 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
31export { 75export {
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
18const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) 18const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
19const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) 19const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
20const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) 20const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
21const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
21 22
22const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 23const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
23const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 24const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
@@ -35,6 +36,7 @@ const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
35const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) 36const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS)
36const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS) 37const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS)
37const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS) 38const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS)
39const 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 @@
1import * as express from 'express'
2import 'express-validator'
3import { body } from 'express-validator/check'
4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils'
6import { isDateValid } from '../../helpers/custom-validators/misc'
7
8const 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
24export {
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 @@
1import * as express from 'express'
2import 'express-validator'
3import { body, query } from 'express-validator/check'
4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils'
6import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
7import { isNotEmptyIntArray } from '../../helpers/custom-validators/misc'
8
9const 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
24const 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
43const 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
59export {
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'
5import { omit } from 'lodash' 5import { omit } from 'lodash'
6import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 6import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
7import { 7import {
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'
18import { isVideoExist } from '../../helpers/custom-validators/videos' 19import { isVideoExist } from '../../helpers/custom-validators/videos'
19import { logger } from '../../helpers/logger' 20import { logger } from '../../helpers/logger'
@@ -22,7 +23,6 @@ import { Redis } from '../../lib/redis'
22import { UserModel } from '../../models/account/user' 23import { UserModel } from '../../models/account/user'
23import { areValidationErrors } from './utils' 24import { areValidationErrors } from './utils'
24import { ActorModel } from '../../models/activitypub/actor' 25import { ActorModel } from '../../models/activitypub/actor'
25import { comparePassword } from '../../helpers/peertube-crypto'
26 26
27const usersAddValidator = [ 27const 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' 3import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../../helpers/custom-validators/videos' 4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { areValidationErrors } from '../utils' 6import { areValidationErrors } from '../utils'
7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' 7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist'
8import { VideoModel } from '../../../models/video/video'
8 9
9const videosBlacklistRemoveValidator = [ 10const 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
23const videosBlacklistAddValidator = [ 24const 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'
4import { isVideoExist } from '../../../helpers/custom-validators/videos' 4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { areValidationErrors } from '../utils' 5import { areValidationErrors } from '../utils'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { UserModel } from '../../../models/account/user'
7 8
8const videoWatchingValidator = [ 9const 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
2import { AccountModel } from './account' 2import { AccountModel } from './account'
3import { getSort } from '../utils' 3import { getSort } from '../utils'
4import { AccountBlock } from '../../../shared/models/blocklist' 4import { AccountBlock } from '../../../shared/models/blocklist'
5import { Op } from 'sequelize'
5 6
6enum ScopeNames { 7enum 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 @@
1import {
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'
15import { throwIfNotValid } from '../utils'
16import { UserModel } from './user'
17import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
18import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
19import { clearCacheByUserId } from '../../lib/oauth-model'
20
21@Table({
22 tableName: 'userNotificationSetting',
23 indexes: [
24 {
25 fields: [ 'userId' ],
26 unique: true
27 }
28 ]
29})
30export 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 @@
1import {
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'
15import { UserNotification, UserNotificationType } from '../../../shared'
16import { getSort, throwIfNotValid } from '../utils'
17import { isBooleanValid } from '../../helpers/custom-validators/misc'
18import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
19import { UserModel } from './user'
20import { VideoModel } from '../video/video'
21import { VideoCommentModel } from '../video/video-comment'
22import { Op } from 'sequelize'
23import { VideoChannelModel } from '../video/video-channel'
24import { AccountModel } from './account'
25import { VideoAbuseModel } from '../video/video-abuse'
26import { VideoBlacklistModel } from '../video/video-blacklist'
27import { VideoImportModel } from '../video/video-import'
28import { ActorModel } from '../activitypub/actor'
29import { ActorFollowModel } from '../activitypub/actor-follow'
30import { AvatarModel } from '../avatar/avatar'
31import { ServerModel } from '../server/server'
32
33enum ScopeNames {
34 WITH_ALL = 'WITH_ALL'
35}
36
37function 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
57function buildVideoInclude (required: boolean) {
58 return {
59 attributes: [ 'id', 'uuid', 'name' ],
60 model: () => VideoModel.unscoped(),
61 required
62 }
63}
64
65function 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
74function 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})
230export 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 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video' 2import { VideoModel } from '../video/video'
3import { UserModel } from './user' 3import { UserModel } from './user'
4import { 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'
37import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 38import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
@@ -43,6 +44,11 @@ import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
43import { values } from 'lodash' 44import { values } from 'lodash'
44import { NSFW_POLICY_TYPES } from '../../initializers' 45import { NSFW_POLICY_TYPES } from '../../initializers'
45import { clearCacheByUserId } from '../../lib/oauth-model' 46import { clearCacheByUserId } from '../../lib/oauth-model'
47import { UserNotificationSettingModel } from './user-notification-setting'
48import { VideoModel } from '../video/video'
49import { ActorModel } from '../activitypub/actor'
50import { ActorFollowModel } from '../activitypub/actor-follow'
51import { VideoImportModel } from '../video/video-import'
46 52
47enum ScopeNames { 53enum 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 {
15import { ActorModel } from '../activitypub/actor' 15import { ActorModel } from '../activitypub/actor'
16import { getVideoSort, throwIfNotValid } from '../utils' 16import { getVideoSort, throwIfNotValid } from '../utils'
17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' 17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' 18import { CONFIG, CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers'
19import { VideoFileModel } from '../video/video-file' 19import { VideoFileModel } from '../video/video-file'
20import { getServerActor } from '../../helpers/utils' 20import { getServerActor } from '../../helpers/utils'
21import { VideoModel } from '../video/video' 21import { 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
35function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) { 39function 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 @@
1import { 1import { 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'
15import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' 2import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
16import { VideoAbuse } from '../../../shared/models/videos' 3import { VideoAbuse } from '../../../shared/models/videos'
17import { 4import {
@@ -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'
22import { Emailer } from '../../lib/emailer'
23import { AccountModel } from '../account/account' 9import { AccountModel } from '../account/account'
24import { getSort, throwIfNotValid } from '../utils' 10import { getSort, throwIfNotValid } from '../utils'
25import { VideoModel } from './video' 11import { VideoModel } from './video'
@@ -40,8 +26,9 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers'
40export class VideoAbuseModel extends Model<VideoAbuseModel> { 26export 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 @@
1import { 1import { 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'
15import { getSortOnModel, SortType, throwIfNotValid } from '../utils' 2import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
16import { VideoModel } from './video' 3import { VideoModel } from './video'
17import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' 4import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
18import { Emailer } from '../../lib/emailer'
19import { VideoBlacklist } from '../../../shared/models/videos' 5import { VideoBlacklist } from '../../../shared/models/videos'
20import { CONSTRAINTS_FIELDS } from '../../initializers' 6import { 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
18import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 18import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
19import { VideoComment } from '../../../shared/models/videos/video-comment.model' 19import { VideoComment } from '../../../shared/models/videos/video-comment.model'
20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
21import { CONSTRAINTS_FIELDS } from '../../initializers' 21import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
22import { sendDeleteVideoComment } from '../../lib/activitypub/send' 22import { sendDeleteVideoComment } from '../../lib/activitypub/send'
23import { AccountModel } from '../account/account' 23import { AccountModel } from '../account/account'
24import { ActorModel } from '../activitypub/actor' 24import { ActorModel } from '../activitypub/actor'
@@ -29,6 +29,9 @@ import { VideoModel } from './video'
29import { VideoChannelModel } from './video-channel' 29import { VideoChannelModel } from './video-channel'
30import { getServerActor } from '../../helpers/utils' 30import { getServerActor } from '../../helpers/utils'
31import { UserModel } from '../account/user' 31import { UserModel } from '../account/user'
32import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
33import { regexpCapture } from '../../helpers/regexp'
34import { uniq } from 'lodash'
32 35
33enum ScopeNames { 36enum 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 @@
1import { values } from 'lodash'
2import { 1import {
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'
16import { 15import {
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'
22import { CONSTRAINTS_FIELDS } from '../../initializers'
23import { throwIfNotValid } from '../utils' 22import { throwIfNotValid } from '../utils'
24import { VideoModel } from './video' 23import { VideoModel } from './video'
25import * as Sequelize from 'sequelize' 24import * 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'
2import { VideoModel } from './video' 2import { VideoModel } from './video'
3import { VideoFileModel } from './video-file' 3import { VideoFileModel } from './video-file'
4import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' 4import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects'
5import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' 5import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers'
6import { VideoCaptionModel } from './video-caption' 6import { VideoCaptionModel } from './video-caption'
7import { 7import {
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 {
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history' 95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user' 96import { UserModel } from '../account/user'
97import { 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
99const indexes: Sequelize.DefineIndexesOptions[] = [ 100const 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
141type AvailableForListIDsOptions = { 170type 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
16const expect = chai.expect 16const 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'
17import * as chai from 'chai' 20import * as chai from 'chai'
18import { setActorField, setVideoField } from '../../utils/miscs/sql'
19import { waitJobs } from '../../utils/server/jobs'
20import { Video } from '../../../../shared/models/videos' 21import { Video } from '../../../../shared/models/videos'
21 22
22const expect = chai.expect 23const 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
3import 'mocha' 3import 'mocha'
4import { expect } from 'chai' 4import { expect } from 'chai'
5import { buildRequestStub } from '../../utils' 5import { buildRequestStub } from '../../../../shared/utils/miscs/stubs'
6import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' 6import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto'
7import { cloneDeep } from 'lodash' 7import { cloneDeep } from 'lodash'
8import { buildSignedActivity } from '../../../helpers/activitypub' 8import { 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
3import 'mocha' 3import 'mocha'
4import { doubleFollow, getVideo, reRunServer } from '../../utils' 4import {
5import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo, wait } from '../../utils/index' 5 doubleFollow,
6import { waitJobs } from '../../utils/server/jobs' 6 flushAndRunMultipleServers,
7import { 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
9describe('Test AP refresher', function () { 18describe('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
3import 'mocha' 3import 'mocha'
4 4
5import { flushAndRunMultipleServers, flushTests, killallServers, ServerInfo } from '../../utils' 5import {
6 flushAndRunMultipleServers,
7 flushTests,
8 killallServers,
9 makeFollowRequest,
10 makePOSTAPRequest,
11 ServerInfo,
12 setActorField
13} from '../../../../shared/utils'
6import { HTTP_SIGNATURE } from '../../../initializers' 14import { HTTP_SIGNATURE } from '../../../initializers'
7import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' 15import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
8import * as chai from 'chai' 16import * as chai from 'chai'
9import { setActorField } from '../../utils/miscs/sql'
10import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' 17import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
11import { makeFollowRequest, makePOSTAPRequest } from '../../utils/requests/activitypub'
12 18
13const expect = chai.expect 19const 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
3import 'mocha' 3import 'mocha'
4 4
5import { flushTests, killallServers, runServer, ServerInfo } from '../../utils' 5import { flushTests, killallServers, runServer, ServerInfo } from '../../../../shared/utils'
6import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 6import {
7import { getAccount } from '../../utils/users/accounts' 7 checkBadCountPagination,
8 8 checkBadSortPagination,
9describe('Test users API validators', function () { 9 checkBadStartPagination
10} from '../../../../shared/utils/requests/check-api-params'
11import { getAccount } from '../../../../shared/utils/users/accounts'
12
13describe('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'
17import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 17import {
18 checkBadCountPagination,
19 checkBadSortPagination,
20 checkBadStartPagination
21} from '../../../../shared/utils/requests/check-api-params'
18 22
19describe('Test blocklist API validators', function () { 23describe('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
7import { 7import {
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
12describe('Test config API validators', function () { 12describe('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
3import 'mocha'
4
5import {
6 flushTests,
7 immutableAssign,
8 killallServers,
9 reRunServer,
10 runServer,
11 ServerInfo,
12 setAccessTokensToServers
13} from '../../../../shared/utils'
14import {
15 checkBadCountPagination,
16 checkBadSortPagination,
17 checkBadStartPagination
18} from '../../../../shared/utils/requests/check-api-params'
19import { getAccount } from '../../../../shared/utils/users/accounts'
20import { sendContactForm } from '../../../../shared/utils/server/contact-form'
21import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
22
23describe('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'
5import { 5import {
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'
9import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 9import {
10 checkBadCountPagination,
11 checkBadSortPagination,
12 checkBadStartPagination
13} from '../../../../shared/utils/requests/check-api-params'
10 14
11describe('Test server follows API validators', function () { 15describe('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
2import './accounts' 1import './accounts'
3import './blocklist' 2import './blocklist'
4import './config' 3import './config'
4import './contact-form'
5import './follows' 5import './follows'
6import './jobs' 6import './jobs'
7import './redundancy' 7import './redundancy'
8import './search' 8import './search'
9import './services' 9import './services'
10import './user-notifications'
10import './user-subscriptions' 11import './user-subscriptions'
11import './users' 12import './users'
12import './video-abuses' 13import './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
3import 'mocha' 3import 'mocha'
4 4
5import { createUser, flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, userLogin } from '../../utils' 5import {
6import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 6 createUser,
7import { makeGetRequest } from '../../utils/requests/requests' 7 flushTests,
8 killallServers,
9 runServer,
10 ServerInfo,
11 setAccessTokensToServers,
12 userLogin
13} from '../../../../shared/utils'
14import {
15 checkBadCountPagination,
16 checkBadSortPagination,
17 checkBadStartPagination
18} from '../../../../shared/utils/requests/check-api-params'
19import { makeGetRequest } from '../../../../shared/utils/requests/requests'
8 20
9describe('Test jobs API validators', function () { 21describe('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
17describe('Test server redundancy API validators', function () { 17describe('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
3import 'mocha' 3import 'mocha'
4 4
5import { flushTests, immutableAssign, killallServers, makeGetRequest, runServer, ServerInfo } from '../../utils' 5import { flushTests, immutableAssign, killallServers, makeGetRequest, runServer, ServerInfo } from '../../../../shared/utils'
6import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 6import {
7 checkBadCountPagination,
8 checkBadSortPagination,
9 checkBadStartPagination
10} from '../../../../shared/utils/requests/check-api-params'
7 11
8describe('Test videos API validator', function () { 12describe('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
3import 'mocha' 3import 'mocha'
4 4
5import { flushTests, killallServers, makeGetRequest, runServer, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils' 5import {
6 flushTests,
7 killallServers,
8 makeGetRequest,
9 runServer,
10 ServerInfo,
11 setAccessTokensToServers,
12 uploadVideo
13} from '../../../../shared/utils'
6 14
7describe('Test services API validators', function () { 15describe('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
3import 'mocha'
4import * as io from 'socket.io-client'
5
6import {
7 flushTests,
8 immutableAssign,
9 killallServers,
10 makeGetRequest,
11 makePostBodyRequest,
12 makePutBodyRequest,
13 runServer,
14 ServerInfo,
15 setAccessTokensToServers,
16 wait
17} from '../../../../shared/utils'
18import {
19 checkBadCountPagination,
20 checkBadSortPagination,
21 checkBadStartPagination
22} from '../../../../shared/utils/requests/check-api-params'
23import { UserNotificationSetting, UserNotificationSettingValue } from '../../../../shared/models/users'
24
25describe('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'
17import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 17
18import { waitJobs } from '../../utils/server/jobs' 18import {
19 checkBadCountPagination,
20 checkBadSortPagination,
21 checkBadStartPagination
22} from '../../../../shared/utils/requests/check-api-params'
23import { waitJobs } from '../../../../shared/utils/server/jobs'
19 24
20describe('Test user subscriptions API validators', function () { 25describe('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'
13import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 13import {
14import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../utils/videos/video-imports' 14 checkBadCountPagination,
15 checkBadSortPagination,
16 checkBadStartPagination
17} from '../../../../shared/utils/requests/check-api-params'
18import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/utils/videos/video-imports'
15import { VideoPrivacy } from '../../../../shared/models/videos' 19import { VideoPrivacy } from '../../../../shared/models/videos'
16import { waitJobs } from '../../utils/server/jobs' 20import { waitJobs } from '../../../../shared/utils/server/jobs'
17import { expect } from 'chai' 21import { expect } from 'chai'
18 22
19describe('Test users API validators', function () { 23describe('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'
19import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 19import {
20 checkBadCountPagination,
21 checkBadSortPagination,
22 checkBadStartPagination
23} from '../../../../shared/utils/requests/check-api-params'
20import { VideoAbuseState } from '../../../../shared/models/videos' 24import { VideoAbuseState } from '../../../../shared/models/videos'
21 25
22describe('Test video abuses API validators', function () { 26describe('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
5import { 5import {
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'
19import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 22import {
23 checkBadCountPagination,
24 checkBadSortPagination,
25 checkBadStartPagination
26} from '../../../../shared/utils/requests/check-api-params'
20import { VideoDetails } from '../../../../shared/models/videos' 27import { VideoDetails } from '../../../../shared/models/videos'
21import { expect } from 'chai' 28import { expect } from 'chai'
22 29
23describe('Test video blacklist API validators', function () { 30describe('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'
17import { join } from 'path' 17import { join } from 'path'
18import { createVideoCaption } from '../../utils/videos/video-captions' 18import { createVideoCaption } from '../../../../shared/utils/videos/video-captions'
19 19
20describe('Test video captions API validator', function () { 20describe('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'
24import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 24import {
25 checkBadCountPagination,
26 checkBadSortPagination,
27 checkBadStartPagination
28} from '../../../../shared/utils/requests/check-api-params'
25import { User } from '../../../../shared/models/users' 29import { User } from '../../../../shared/models/users'
26import { join } from 'path' 30import { 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'
10import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 10import {
11import { addVideoCommentThread } from '../../utils/videos/video-comments' 11 checkBadCountPagination,
12 checkBadSortPagination,
13 checkBadStartPagination
14} from '../../../../shared/utils/requests/check-api-params'
15import { addVideoCommentThread } from '../../../../shared/utils/videos/video-comments'
12 16
13const expect = chai.expect 17const 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'
22import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 22import {
23import { getMagnetURI, getYoutubeVideoUrl } from '../../utils/videos/video-imports' 23 checkBadCountPagination,
24 checkBadSortPagination,
25 checkBadStartPagination
26} from '../../../../shared/utils/requests/check-api-params'
27import { getMagnetURI, getYoutubeVideoUrl } from '../../../../shared/utils/videos/video-imports'
24 28
25describe('Test video imports API validator', function () { 29describe('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'
15import { UserRole } from '../../../../shared/models/users' 15import { UserRole } from '../../../../shared/models/users'
16 16
17const expect = chai.expect 17const 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 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
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
16const expect = chai.expect 19const expect = chai.expect
17 20
18describe('Test videos history API validator', function () { 21describe('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
8import { 8import {
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'
12import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 12import {
13import { getAccountsList } from '../../utils/users/accounts' 13 checkBadCountPagination,
14 checkBadSortPagination,
15 checkBadStartPagination
16} from '../../../../shared/utils/requests/check-api-params'
17import { getAccountsList } from '../../../../shared/utils/users/accounts'
14 18
15const expect = chai.expect 19const 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'
22import { waitJobs } from '../../utils/server/jobs' 22import { waitJobs } from '../../../../shared/utils/server/jobs'
23
23import * as magnetUtil from 'magnet-uri' 24import * as magnetUtil from 'magnet-uri'
24import { updateRedundancy } from '../../utils/server/redundancy' 25import { updateRedundancy } from '../../../../shared/utils/server/redundancy'
25import { ActorFollow } from '../../../../shared/models/actors' 26import { ActorFollow } from '../../../../shared/models/actors'
26import { readdir } from 'fs-extra' 27import { readdir } from 'fs-extra'
27import { join } from 'path' 28import { join } from 'path'
28import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy' 29import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy'
29import { getStats } from '../../utils/server/stats' 30import { getStats } from '../../../../shared/utils/server/stats'
30import { ServerStats } from '../../../../shared/models/server/server-stats.model' 31import { ServerStats } from '../../../../shared/models/server/server-stats.model'
31 32
32const expect = chai.expect 33const 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'
21import { waitJobs } from '../../utils/server/jobs' 21import { waitJobs } from '../../../../shared/utils/server/jobs'
22import { VideoChannel } from '../../../../shared/models/videos' 22import { VideoChannel } from '../../../../shared/models/videos'
23import { searchVideoChannel } from '../../utils/search/video-channels' 23import { searchVideoChannel } from '../../../../shared/utils/search/video-channels'
24 24
25const expect = chai.expect 25const 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'
20import { waitJobs } from '../../utils/server/jobs' 20import { waitJobs } from '../../../../shared/utils/server/jobs'
21import { Video, VideoPrivacy } from '../../../../shared/models/videos' 21import { Video, VideoPrivacy } from '../../../../shared/models/videos'
22 22
23const expect = chai.expect 23const 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
18const expect = chai.expect 18const 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'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { About } from '../../../../shared/models/server/about.model' 5import { About } from '../../../../shared/models/server/about.model'
6import { CustomConfig } from '../../../../shared/models/server/custom-config.model' 6import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
7import { deleteCustomConfig, getAbout, killallServers, reRunServer } from '../../utils'
8import { 7import {
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'
20import { ServerConfig } from '../../../../shared/models'
17 21
18const expect = chai.expect 22const 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
3import * as chai from 'chai'
4import 'mocha'
5import { flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, wait } from '../../../../shared/utils'
6import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
7import { waitJobs } from '../../../../shared/utils/server/jobs'
8import { sendContactForm } from '../../../../shared/utils/server/contact-form'
9
10const expect = chai.expect
11
12describe('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,
19import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' 19 killallServers,
20import { mockSmtpServer } from '../../utils/miscs/email' 20 ServerInfo,
21import { waitJobs } from '../../utils/server/jobs' 21 setAccessTokensToServers
22} from '../../../../shared/utils'
23import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
24import { waitJobs } from '../../../../shared/utils/server/jobs'
22 25
23const expect = chai.expect 26const 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
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { doubleFollow, getAccountVideos, getVideo, getVideoChannelVideos, getVideoWithToken } from '../../utils' 5import {
6import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index' 6 doubleFollow,
7import { unfollow } from '../../utils/server/follows' 7 getAccountVideos,
8import { userLogin } from '../../utils/users/login' 8 getVideo,
9import { createUser } from '../../utils/users/users' 9 getVideoChannelVideos,
10 getVideoWithToken,
11 flushAndRunMultipleServers,
12 killallServers,
13 ServerInfo,
14 setAccessTokensToServers,
15 uploadVideo
16} from '../../../../shared/utils'
17import { unfollow } from '../../../../shared/utils/server/follows'
18import { userLogin } from '../../../../shared/utils/users/login'
19import { createUser } from '../../../../shared/utils/users/users'
10 20
11const expect = chai.expect 21const 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'
4import 'mocha' 4import 'mocha'
5import { Video, VideoPrivacy } from '../../../../shared/models/videos' 5import { Video, VideoPrivacy } from '../../../../shared/models/videos'
6import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 6import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
7import { completeVideoCheck } from '../../utils' 7import { completeVideoCheck } from '../../../../shared/utils'
8import { 8import {
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'
16import { dateIsValid } from '../../utils/miscs/miscs' 16import { dateIsValid } from '../../../../shared/utils/miscs/miscs'
17import { follow, getFollowersListPaginationAndSort, getFollowingListPaginationAndSort, unfollow } from '../../utils/server/follows' 17import {
18import { expectAccountFollows } from '../../utils/users/accounts' 18 follow,
19import { userLogin } from '../../utils/users/login' 19 getFollowersListPaginationAndSort,
20import { createUser } from '../../utils/users/users' 20 getFollowingListPaginationAndSort,
21 unfollow
22} from '../../../../shared/utils/server/follows'
23import { expectAccountFollows } from '../../../../shared/utils/users/accounts'
24import { userLogin } from '../../../../shared/utils/users/login'
25import { createUser } from '../../../../shared/utils/users/users'
21import { 26import {
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'
27import { rateVideo } from '../../utils/videos/videos' 32import { rateVideo } from '../../../../shared/utils/videos/videos'
28import { waitJobs } from '../../utils/server/jobs' 33import { waitJobs } from '../../../../shared/utils/server/jobs'
29import { createVideoCaption, listVideoCaptions, testCaptionFile } from '../../utils/videos/video-captions' 34import { createVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/utils/videos/video-captions'
30import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' 35import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
31 36
32const expect = chai.expect 37const 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'
5import { JobState, Video } from '../../../../shared/models' 5import { JobState, Video } from '../../../../shared/models'
6import { VideoPrivacy } from '../../../../shared/models/videos' 6import { VideoPrivacy } from '../../../../shared/models/videos'
7import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 7import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
8import { completeVideoCheck, getVideo, immutableAssign, reRunServer, unfollow, updateVideo, viewVideo } from '../../utils' 8
9import { 9import {
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'
18import { follow, getFollowersListPaginationAndSort } from '../../utils/server/follows' 24import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows'
19import { getJobsListPaginationAndSort, waitJobs } from '../../utils/server/jobs' 25import { getJobsListPaginationAndSort, waitJobs } from '../../../../shared/utils/server/jobs'
20import { 26import {
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
27const expect = chai.expect 33const 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 @@
1import './config' 1import './config'
2import './contact-form'
2import './email' 3import './email'
3import './follow-constraints' 4import './follow-constraints'
4import './follows' 5import './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
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' 5import { killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index'
6import { doubleFollow } from '../../utils/server/follows' 6import { doubleFollow } from '../../../../shared/utils/server/follows'
7import { getJobsList, getJobsListPaginationAndSort, waitJobs } from '../../utils/server/jobs' 7import { getJobsList, getJobsListPaginationAndSort, waitJobs } from '../../../../shared/utils/server/jobs'
8import { flushAndRunMultipleServers } from '../../utils/server/servers' 8import { flushAndRunMultipleServers } from '../../../../shared/utils/server/servers'
9import { uploadVideo } from '../../utils/videos/videos' 9import { uploadVideo } from '../../../../shared/utils/videos/videos'
10import { dateIsValid } from '../../utils/miscs/miscs' 10import { dateIsValid } from '../../../../shared/utils/miscs/miscs'
11 11
12const expect = chai.expect 12const 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'
8import { runServer } from '../../utils/server/servers' 8import { runServer } from '../../../../shared/utils/server/servers'
9 9
10describe('Start and stop server without web client routes', function () { 10describe('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'
19const expect = chai.expect 19const expect = chai.expect
20 20
21import { 21import {
@@ -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
28describe('Test application behind a reverse proxy', function () { 28describe('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'
17import { flushTests, setAccessTokensToServers } from '../../utils/index' 17import { flushTests, setAccessTokensToServers } from '../../../../shared/utils/index'
18import { getStats } from '../../utils/server/stats' 18import { getStats } from '../../../../shared/utils/server/stats'
19import { addVideoCommentThread } from '../../utils/videos/video-comments' 19import { addVideoCommentThread } from '../../../../shared/utils/videos/video-comments'
20import { waitJobs } from '../../utils/server/jobs' 20import { waitJobs } from '../../../../shared/utils/server/jobs'
21 21
22const expect = chai.expect 22const 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
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import 'mocha' 4import 'mocha'
5import { getVideo, killallServers, runServer, ServerInfo, uploadVideo } from '../../utils' 5import { getVideo, killallServers, runServer, ServerInfo, uploadVideo } from '../../../../shared/utils'
6import { flushTests, setAccessTokensToServers } from '../../utils/index' 6import { flushTests, setAccessTokensToServers } from '../../../../shared/utils/index'
7import { VideoDetails } from '../../../../shared/models/videos' 7import { VideoDetails } from '../../../../shared/models/videos'
8import * as WebTorrent from 'webtorrent' 8import * 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'
16import { setAccessTokensToServers } from '../../utils/users/login' 16import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
17import { getVideosListWithToken, getVideosList } from '../../utils/videos/videos' 17import { getVideosListWithToken, getVideosList } from '../../../../shared/utils/videos/videos'
18import { 18import {
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'
24import { waitJobs } from '../../utils/server/jobs' 24import { waitJobs } from '../../../../shared/utils/server/jobs'
25import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 25import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
26import { 26import {
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
41const expect = chai.expect 41const 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 @@
1import './users-verification'
2import './user-notifications'
1import './blocklist' 3import './blocklist'
2import './user-subscriptions' 4import './user-subscriptions'
3import './users' 5import './users'
4import './users-multiple-servers' 6import './users-multiple-servers'
5import './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
3import * as chai from 'chai'
4import 'mocha'
5import {
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'
22import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index'
23import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
24import { waitJobs } from '../../../../shared/utils/server/jobs'
25import { getUserNotificationSocket } from '../../../../shared/utils/socket/socket-io'
26import {
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'
43import {
44 User,
45 UserNotification,
46 UserNotificationSetting,
47 UserNotificationSettingValue,
48 UserNotificationType
49} from '../../../../shared/models/users'
50import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
51import { addUserSubscription, removeUserSubscription } from '../../../../shared/utils/users/user-subscriptions'
52import { VideoPrivacy } from '../../../../shared/models/videos'
53import { getBadVideoUrl, getYoutubeVideoUrl, importVideo } from '../../../../shared/utils/videos/video-imports'
54import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments'
55import * as uuidv4 from 'uuid/v4'
56import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist'
57
58const expect = chai.expect
59
60async 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
71async 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
82describe('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
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { createUser, doubleFollow, flushAndRunMultipleServers, follow, getVideosList, unfollow, updateVideo, userLogin } from '../../utils' 5import {
6import { killallServers, ServerInfo, uploadVideo } from '../../utils/index' 6 createUser,
7import { setAccessTokensToServers } from '../../utils/users/login' 7 doubleFollow,
8 flushAndRunMultipleServers,
9 follow,
10 getVideosList,
11 unfollow,
12 updateVideo,
13 userLogin
14} from '../../../../shared/utils'
15import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index'
16import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
8import { Video, VideoChannel } from '../../../../shared/models/videos' 17import { Video, VideoChannel } from '../../../../shared/models/videos'
9import { waitJobs } from '../../utils/server/jobs' 18import { waitJobs } from '../../../../shared/utils/server/jobs'
10import { 19import {
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
18const expect = chai.expect 27const 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'
17import { getMyUserInformation, killallServers, ServerInfo, testImage, updateMyAvatar, uploadVideo } from '../../utils/index' 17import { getMyUserInformation, killallServers, ServerInfo, testImage, updateMyAvatar, uploadVideo } from '../../../../shared/utils/index'
18import { checkActorFilesWereRemoved, getAccount, getAccountsList } from '../../utils/users/accounts' 18import { checkActorFilesWereRemoved, getAccount, getAccountsList } from '../../../../shared/utils/users/accounts'
19import { setAccessTokensToServers } from '../../utils/users/login' 19import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
20import { User } from '../../../../shared/models/users' 20import { User } from '../../../../shared/models/users'
21import { VideoChannel } from '../../../../shared/models/videos' 21import { VideoChannel } from '../../../../shared/models/videos'
22import { waitJobs } from '../../utils/server/jobs' 22import { waitJobs } from '../../../../shared/utils/server/jobs'
23 23
24const expect = chai.expect 24const 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'
4import 'mocha' 4import 'mocha'
5import { 5import {
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'
9import { setAccessTokensToServers } from '../../utils/users/login' 9import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
10import { mockSmtpServer } from '../../utils/miscs/email' 10import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
11import { waitJobs } from '../../utils/server/jobs' 11import { waitJobs } from '../../../../shared/utils/server/jobs'
12 12
13const expect = chai.expect 13const 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'
36import { follow } from '../../utils/server/follows' 36import { follow } from '../../../../shared/utils/server/follows'
37import { setAccessTokensToServers } from '../../utils/users/login' 37import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
38import { getMyVideos } from '../../utils/videos/videos' 38import { getMyVideos } from '../../../../shared/utils/videos/videos'
39 39
40const expect = chai.expect 40const 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'
3import './single-server' 3import './single-server'
4import './video-abuse' 4import './video-abuse'
5import './video-blacklist' 5import './video-blacklist'
6import './video-blacklist-management'
7import './video-captions' 6import './video-captions'
8import './video-change-ownership' 7import './video-change-ownership'
9import './video-channels' 8import './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'
8import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 8import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
9import { 9import {
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'
35import { 36import {
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'
42import { waitJobs } from '../../utils/server/jobs' 43import { waitJobs } from '../../../../shared/utils/server/jobs'
43 44
44const expect = chai.expect 45const 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
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { flushTests, getOEmbed, getVideosList, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index' 5import {
6import { runServer } from '../../utils/server/servers' 6 flushTests,
7 getOEmbed,
8 getVideosList,
9 killallServers,
10 ServerInfo,
11 setAccessTokensToServers,
12 uploadVideo
13} from '../../../../shared/utils/index'
14import { runServer } from '../../../../shared/utils/server/servers'
7 15
8const expect = chai.expect 16const 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
33const expect = chai.expect 33const 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'
18import { doubleFollow } from '../../utils/server/follows' 18import { doubleFollow } from '../../../../shared/utils/server/follows'
19import { waitJobs } from '../../utils/server/jobs' 19import { waitJobs } from '../../../../shared/utils/server/jobs'
20 20
21const expect = chai.expect 21const 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
3import * as chai from 'chai'
4import { orderBy } from 'lodash'
5import 'mocha'
6import {
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'
20import { doubleFollow } from '../../utils/server/follows'
21import { waitJobs } from '../../utils/server/jobs'
22import { VideoAbuse } from '../../../../shared/models/videos'
23
24const expect = chai.expect
25
26describe('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
3import * as chai from 'chai' 3import * as chai from 'chai'
4import { orderBy } from 'lodash'
4import 'mocha' 5import 'mocha'
5import { 6import {
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,
15import { doubleFollow } from '../../utils/server/follows' 20 uploadVideo,
16import { waitJobs } from '../../utils/server/jobs' 21 viewVideo
22} from '../../../../shared/utils/index'
23import { doubleFollow } from '../../../../shared/utils/server/follows'
24import { waitJobs } from '../../../../shared/utils/server/jobs'
25import { VideoBlacklist } from '../../../../shared/models/videos'
17 26
18const expect = chai.expect 27const expect = chai.expect
19 28
20describe('Test video blacklists', function () { 29describe('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
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { checkVideoFilesWereRemoved, doubleFollow, flushAndRunMultipleServers, removeVideo, uploadVideo, wait } from '../../utils' 5import {
6import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' 6 checkVideoFilesWereRemoved,
7import { waitJobs } from '../../utils/server/jobs' 7 doubleFollow,
8import { createVideoCaption, deleteVideoCaption, listVideoCaptions, testCaptionFile } from '../../utils/videos/video-captions' 8 flushAndRunMultipleServers,
9 removeVideo,
10 uploadVideo,
11 wait
12} from '../../../../shared/utils'
13import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index'
14import { waitJobs } from '../../../../shared/utils/server/jobs'
15import { createVideoCaption, deleteVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/utils/videos/video-captions'
9import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' 16import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
10 17
11const expect = chai.expect 18const 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'
22import { waitJobs } from '../../utils/server/jobs' 22import { waitJobs } from '../../../../shared/utils/server/jobs'
23import { User } from '../../../../shared/models/users' 23import { User } from '../../../../shared/models/users'
24import { VideoDetails } from '../../../../shared/models/videos' 24import { 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'
17import { 17import {
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'
30import { waitJobs } from '../../utils/server/jobs' 30import { waitJobs } from '../../../../shared/utils/server/jobs'
31 31
32const expect = chai.expect 32const 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 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 5import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
6import { testImage } from '../../utils' 6import { testImage } from '../../../../shared/utils'
7import { 7import {
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'
17import { 17import {
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
25const expect = chai.expect 25const 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'
16import { doubleFollow } from '../../utils/server/follows' 16import { doubleFollow } from '../../../../shared/utils/server/follows'
17import { waitJobs } from '../../utils/server/jobs' 17import { waitJobs } from '../../../../shared/utils/server/jobs'
18 18
19const expect = chai.expect 19const 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'
18import { waitJobs } from '../../utils/server/jobs' 18import { waitJobs } from '../../../../shared/utils/server/jobs'
19import { getMagnetURI, getYoutubeVideoUrl, importVideo, getMyVideoImports } from '../../utils/videos/video-imports' 19import { getMagnetURI, getYoutubeVideoUrl, importVideo, getMyVideoImports } from '../../../../shared/utils/videos/video-imports'
20 20
21const expect = chai.expect 21const 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
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { flushTests, getVideosList, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index' 5import {
6import { userLogin } from '../../utils/users/login' 6 flushTests,
7import { createUser } from '../../utils/users/users' 7 getVideosList,
8import { getMyVideos } from '../../utils/videos/videos' 8 killallServers,
9 ServerInfo,
10 setAccessTokensToServers,
11 uploadVideo
12} from '../../../../shared/utils/index'
13import { userLogin } from '../../../../shared/utils/users/login'
14import { createUser } from '../../../../shared/utils/users/users'
15import { getMyVideos } from '../../../../shared/utils/videos/videos'
9import { 16import {
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'
22import { ServerConfig } from '../../../../shared/models' 29import { ServerConfig } from '../../../../shared/models'
23import { CustomConfig } from '../../../../shared/models/server/custom-config.model' 30import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
24import { User } from '../../../../shared/models/users' 31import { 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'
14import { doubleFollow } from '../../utils/server/follows' 14import { doubleFollow } from '../../../../shared/utils/server/follows'
15import { userLogin } from '../../utils/users/login' 15import { userLogin } from '../../../../shared/utils/users/login'
16import { createUser } from '../../utils/users/users' 16import { createUser } from '../../../../shared/utils/users/users'
17import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../utils/videos/videos' 17import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/utils/videos/videos'
18import { waitJobs } from '../../utils/server/jobs' 18import { waitJobs } from '../../../../shared/utils/server/jobs'
19 19
20const expect = chai.expect 20const 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'
19import { waitJobs } from '../../utils/server/jobs' 19import { waitJobs } from '../../../../shared/utils/server/jobs'
20 20
21const expect = chai.expect 21const 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'
23import { join } from 'path' 23import { extname, join } from 'path'
24import { waitJobs } from '../../utils/server/jobs' 24import { waitJobs } from '../../../../shared/utils/server/jobs'
25import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' 25import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
26 26
27const expect = chai.expect 27const 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'
17import { Video, VideoPrivacy } from '../../../../shared/models/videos' 17import { Video, VideoPrivacy } from '../../../../shared/models/videos'
18import { UserRole } from '../../../../shared/models/users' 18import { 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 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
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'
15import { Video, VideoDetails } from '../../../../shared/models/videos' 19import { Video, VideoDetails } from '../../../../shared/models/videos'
16import { userWatchVideo } from '../../utils/videos/video-history' 20import { listMyVideosHistory, removeMyVideosHistory, userWatchVideo } from '../../../../shared/utils/videos/video-history'
17 21
18const expect = chai.expect 22const 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
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils' 5import { flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../../../shared/utils'
6import { getVideosOverview } from '../../utils/overviews/overviews' 6import { getVideosOverview } from '../../../../shared/utils/overviews/overviews'
7import { VideosOverview } from '../../../../shared/models/overviews' 7import { VideosOverview } from '../../../../shared/models/overviews'
8 8
9const expect = chai.expect 9const 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'
19import { waitJobs } from '../utils/server/jobs' 19import { waitJobs } from '../../../shared/utils/server/jobs'
20 20
21const expect = chai.expect 21const 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'
19import { waitJobs } from '../utils/server/jobs' 19import { waitJobs } from '../../../shared/utils/server/jobs'
20 20
21const expect = chai.expect 21const 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
2import './create-import-video-file-job' 2import './create-import-video-file-job'
3import './create-transcoding-job' 3import './create-transcoding-job'
4import './optimize-old-videos'
4import './peertube' 5import './peertube'
5import './reset-password' 6import './reset-password'
6import './update-host' 7import './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'
19import { waitJobs } from '../utils/server/jobs' 19import { waitJobs } from '../../../shared/utils/server/jobs'
20import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils' 20import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils'
21import { VIDEO_TRANSCODING_FPS } from '../../initializers' 21import { VIDEO_TRANSCODING_FPS } from '../../initializers'
22import { join } from 'path' 22import { 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
16describe('Test CLI wrapper', function () { 16describe('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
15describe('Test reset password scripts', function () { 15describe('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 @@
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { VideoDetails } from '../../../shared/models/videos' 5import { VideoDetails } from '../../../shared/models/videos'
6import { waitJobs } from '../utils/server/jobs' 6import { waitJobs } from '../../../shared/utils/server/jobs'
7import { addVideoCommentThread } from '../utils/videos/video-comments' 7import { addVideoCommentThread } from '../../../shared/utils/videos/video-comments'
8import { 8import {
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'
25import { getAccountsList } from '../utils/users/accounts' 25import { getAccountsList } from '../../../shared/utils/users/accounts'
26 26
27const expect = chai.expect 27const 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
20const expect = chai.expect 20const 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'
17import * as libxmljs from 'libxmljs' 17import * as libxmljs from 'libxmljs'
18import { addVideoCommentThread } from '../utils/videos/video-comments' 18import { addVideoCommentThread } from '../../../shared/utils/videos/video-comments'
19import { waitJobs } from '../utils/server/jobs' 19import { waitJobs } from '../../../shared/utils/server/jobs'
20import { User } from '../../../shared/models/users' 20import { User } from '../../../shared/models/users'
21 21
22chai.use(require('chai-xml')) 22chai.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
3import * as chai from 'chai'
4import 'mocha'
5import { VideoCommentModel } from '../../models/video/video-comment'
6
7const expect = chai.expect
8
9class CommentMock {
10 text: string
11
12 extractMentions = VideoCommentModel.prototype.extractMentions
13}
14
15describe('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
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { snakeCase, isNumber } from 'lodash'
5import { 6import {
6 parseBytes 7 parseBytes, objectConverter
7} from '../../helpers/core-utils' 8} from '../../helpers/core-utils'
9import { isNumeric } from 'validator'
8 10
9const expect = chai.expect 11const expect = chai.expect
10 12
11describe('Parse Bytes', function () { 13describe('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 @@
1import './core-utils' 1import './core-utils'
2import './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
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { flushTests, killallServers, makeGetRequest, runServer, ServerInfo } from './utils' 5import {
6 addVideoChannel,
7 createUser,
8 flushTests,
9 killallServers,
10 makeGetRequest,
11 runServer,
12 ServerInfo,
13 setAccessTokensToServers,
14 uploadVideo
15} from '../../shared/utils'
16import { VideoPrivacy } from '../../shared/models/videos'
6 17
7const expect = chai.expect 18const 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'
14import * as Bluebird from 'bluebird' 14import * as Bluebird from 'bluebird'
15 15
16start() 16start()
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'
20import { getJobsListPaginationAndSort } from '../utils/server/jobs' 20import { getJobsListPaginationAndSort } from '../../../shared/utils/server/jobs'
21 21
22interface ServerInfo extends DefaultServerInfo { 22interface 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 @@
1import { exec } from 'child_process'
2
3import { ServerInfo } from '../server/servers'
4
5function getEnvCli (server?: ServerInfo) {
6 return `NODE_ENV=test NODE_APP_INSTANCE=${server.serverNumber}`
7}
8
9async 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
21export {
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 @@
1import * as request from 'supertest'
2
3type FeedType = 'videos' | 'video-comments'
4
5function 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
16function 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
29export {
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 @@
1export * from './server/activitypub'
2export * from './cli/cli'
3export * from './server/clients'
4export * from './server/config'
5export * from './users/login'
6export * from './miscs/miscs'
7export * from './miscs/stubs'
8export * from './server/follows'
9export * from './requests/requests'
10export * from './server/servers'
11export * from './videos/services'
12export * from './users/users'
13export * from './videos/video-abuses'
14export * from './videos/video-blacklist'
15export * from './videos/video-channels'
16export * from './videos/videos'
17export * from './videos/video-change-ownership'
18export * from './feeds/feeds'
19export * 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 @@
1import * as MailDev from 'maildev'
2
3function 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
23export {
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
3import * as chai from 'chai'
4import { isAbsolute, join } from 'path'
5import * as request from 'supertest'
6import * as WebTorrent from 'webtorrent'
7import { pathExists, readFile } from 'fs-extra'
8import * as ffmpeg from 'fluent-ffmpeg'
9
10const expect = chai.expect
11let webtorrent = new WebTorrent()
12
13function immutableAssign <T, U> (target: T, source: U) {
14 return Object.assign<{}, T, U>({}, target, source)
15}
16
17 // Default interval -> 5 minutes
18function 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
25function wait (milliseconds: number) {
26 return new Promise(resolve => setTimeout(resolve, milliseconds))
27}
28
29function 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
35function root () {
36 // We are in server/tests/utils/miscs
37 return join(__dirname, '..', '..', '..', '..')
38}
39
40async 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
55function 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
65async 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
92export {
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 @@
1import * as Sequelize from 'sequelize'
2
3function 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
19function 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
27function 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
35export {
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 @@
1function buildRequestStub (): any {
2 return { }
3}
4
5function buildResponseStub (): any {
6 return {
7 locals: {}
8 }
9}
10
11export {
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 @@
1import { makeGetRequest } from '../requests/requests'
2
3function 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
18export { 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 @@
1import { doRequest } from '../../../helpers/requests'
2import { HTTP_SIGNATURE } from '../../../initializers'
3import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
4import { activityPubContextify } from '../../../helpers/activitypub'
5
6function 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
18async 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
40export {
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 @@
1import { makeGetRequest } from './requests'
2import { immutableAssign } from '../miscs/miscs'
3
4function 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
14function 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
24function 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
36export {
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 @@
1import * as request from 'supertest'
2import { buildAbsoluteFixturePath } from '../miscs/miscs'
3import { isAbsolute, join } from 'path'
4
5function 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
26function 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
43function 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
89function 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
109function 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
128function makeHTMLRequest (url: string, path: string) {
129 return request(url)
130 .get(path)
131 .set('Accept', 'text/html')
132 .expect(200)
133}
134
135function 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
160export {
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 @@
1import { makeGetRequest } from '../requests/requests'
2
3function 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
20export {
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
3import * as request from 'supertest'
4import { VideosSearchQuery } from '../../../../shared/models/search'
5import { immutableAssign } from '../miscs/miscs'
6
7function 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
18function 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
30function 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
46function 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
58function 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
71export {
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 @@
1import * as request from 'supertest'
2
3function 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
12export {
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 @@
1import * as request from 'supertest'
2import * as urlUtil from 'url'
3
4function 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
17export {
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 @@
1import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
2import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
3
4function getConfig (url: string) {
5 const path = '/api/v1/config'
6
7 return makeGetRequest({
8 url,
9 path,
10 statusCodeExpected: 200
11 })
12}
13
14function getAbout (url: string) {
15 const path = '/api/v1/config/about'
16
17 return makeGetRequest({
18 url,
19 path,
20 statusCodeExpected: 200
21 })
22}
23
24function 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
35function 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
47function 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
115function 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
128export {
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 @@
1import * as request from 'supertest'
2import { ServerInfo } from './servers'
3import { waitJobs } from './jobs'
4
5function 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
19function 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
33async 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
47async 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
59async 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
73export {
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 @@
1import * as request from 'supertest'
2import { Job, JobState } from '../../../../shared/models'
3import { ServerInfo } from './servers'
4import { wait } from '../miscs/miscs'
5
6function 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
17function 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
31async 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
74export {
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 @@
1import { makePutBodyRequest } from '../requests/requests'
2
3async 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
15export {
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 @@
1import { ChildProcess, exec, fork } from 'child_process'
2import { join } from 'path'
3import { root, wait } from '../miscs/miscs'
4import { readFile } from 'fs-extra'
5
6interface 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
40function 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
62function 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
72function 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
149async 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
156function killallServers (servers: ServerInfo[]) {
157 for (const server of servers) {
158 process.kill(-server.app.pid)
159 }
160}
161
162async 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
177export {
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 @@
1import { makeGetRequest } from '../requests/requests'
2
3function 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
20export {
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
3import { expect } from 'chai'
4import { existsSync, readdir } from 'fs-extra'
5import { join } from 'path'
6import { Account } from '../../../../shared/models/actors'
7import { root } from '../miscs/miscs'
8import { makeGetRequest } from '../requests/requests'
9
10function 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
21function 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
31async 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
40async 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
58export {
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
3import { makeGetRequest, makeDeleteRequest, makePostBodyRequest } from '../requests/requests'
4
5function 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
24function 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
38function 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
49function 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
68function 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
82function 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
93function 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
112function 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
126function 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
137function 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
156function 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
170function 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
183export {
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 @@
1import * as request from 'supertest'
2
3import { ServerInfo } from '../server/servers'
4
5type Client = { id: string, secret: string }
6type User = { username: string, password: string }
7type Server = { url: string, client: Client, user: User }
8
9function 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
29async 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
35async 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
41function 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
54export {
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 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest } from '../requests/requests'
2
3function 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
15function 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
27function 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
39function 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
50function 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
61function areSubscriptionsExist (url: string, token: string, uris: string[], statusCodeExpected = 200) {
62 const path = '/api/v1/users/me/subscriptions/exist'
63
64 return makeGetRequest({
65 url,
66 path,
67 query: { 'uris[]': uris },
68 token,
69 statusCodeExpected
70 })
71}
72
73// ---------------------------------------------------------------------------
74
75export {
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 @@
1import * as request from 'supertest'
2import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests'
3
4import { UserRole } from '../../../../shared/index'
5import { NSFWPolicyType } from '../../../../shared/models/videos/nsfw-policy.type'
6
7function 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
35function 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
50function 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
61function 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
71function 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
82function 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
93function 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
104function 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
115function 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
130function 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
140function 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
153function 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
163function 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
194function 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
204function 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
232function 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
243function 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
254function 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
265function 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
278export {
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 @@
1import * as request from 'supertest'
2
3function 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
21export {
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 @@
1import * as request from 'supertest'
2import { VideoAbuseUpdate } from '../../../../shared/models/videos/abuse/video-abuse-update.model'
3import { makeDeleteRequest, makePutBodyRequest } from '../requests/requests'
4
5function 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
16function 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
28function 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
47function 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
60export {
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 @@
1import * as request from 'supertest'
2
3function 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
14function 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
25function 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
35function 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
47function 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
61export {
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 @@
1import { makeDeleteRequest, makeGetRequest, makeUploadRequest } from '../requests/requests'
2import * as request from 'supertest'
3import * as chai from 'chai'
4import { buildAbsoluteFixturePath } from '../miscs/miscs'
5
6const expect = chai.expect
7
8function 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
35function 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
45function 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
56async 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
66export {
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 @@
1import * as request from 'supertest'
2
3function 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
14function 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
26function 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
37function 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
49export {
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 @@
1import * as request from 'supertest'
2import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared/models/videos'
3import { updateAvatarRequest } from '../requests/requests'
4
5function 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
20function 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
30function 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
54function 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
76function 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
86function 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
96function 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
110export {
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 @@
1import * as request from 'supertest'
2import { makeDeleteRequest } from '../requests/requests'
3
4function 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
20function 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
33function 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
44function 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
62function 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
81export {
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 @@
1import { makePutBodyRequest } from '../requests/requests'
2
3function 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
12export {
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 @@
1import { VideoImportCreate } from '../../../../shared/models/videos'
2import { makeGetRequest, makeUploadRequest } from '../requests/requests'
3
4function getYoutubeVideoUrl () {
5 return 'https://youtu.be/msX3jv1XdvM'
6}
7
8function 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
13function 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
29function 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
46export {
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
3import { expect } from 'chai'
4import { existsSync, readdir, readFile } from 'fs-extra'
5import * as parseTorrent from 'parse-torrent'
6import { extname, join } from 'path'
7import * as request from 'supertest'
8import {
9 buildAbsoluteFixturePath,
10 getMyUserInformation,
11 immutableAssign,
12 makeGetRequest,
13 makePutBodyRequest,
14 makeUploadRequest,
15 root,
16 ServerInfo,
17 testImage
18} from '../'
19import { VideoDetails, VideoPrivacy } from '../../../../shared/models/videos'
20import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants'
21import { dateIsValid, webtorrentAdd } from '../miscs/miscs'
22
23type 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
44function getVideoCategories (url: string) {
45 const path = '/api/v1/videos/categories'
46
47 return makeGetRequest({
48 url,
49 path,
50 statusCodeExpected: 200
51 })
52}
53
54function getVideoLicences (url: string) {
55 const path = '/api/v1/videos/licences'
56
57 return makeGetRequest({
58 url,
59 path,
60 statusCodeExpected: 200
61 })
62}
63
64function getVideoLanguages (url: string) {
65 const path = '/api/v1/videos/languages'
66
67 return makeGetRequest({
68 url,
69 path,
70 statusCodeExpected: 200
71 })
72}
73
74function getVideoPrivacies (url: string) {
75 const path = '/api/v1/videos/privacies'
76
77 return makeGetRequest({
78 url,
79 path,
80 statusCodeExpected: 200
81 })
82}
83
84function 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
93function 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
107function 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
117function 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
125function 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
136function 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
148function 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
159function 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
175function 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
199function 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
223function 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
238function 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
249function 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
260function 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
270async 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
290async 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
363function 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
405function 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
416function 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
428async 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
551export {
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
11program 11program
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'
6import { VideoPrivacy } from '../../shared/models/videos' 6import { VideoPrivacy } from '../../shared/models/videos'
7import { doRequestAndSaveToFile } from '../helpers/requests' 7import { doRequestAndSaveToFile } from '../helpers/requests'
8import { CONSTRAINTS_FIELDS } from '../initializers' 8import { CONSTRAINTS_FIELDS } from '../initializers'
9import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../tests/utils' 9import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/utils/index'
10import { truncate } from 'lodash' 10import { truncate } from 'lodash'
11import * as prompt from 'prompt' 11import * as prompt from 'prompt'
12import { remove } from 'fs-extra' 12import { 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
80async function promptPassword () { 88async 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
337function 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 @@
1import * as program from 'commander' 1import * as program from 'commander'
2import { access, constants } from 'fs-extra' 2import { access, constants } from 'fs-extra'
3import { isAbsolute } from 'path' 3import { isAbsolute } from 'path'
4import { getClient, login } from '../tests/utils' 4import { getClient, login } from '../../shared/utils'
5import { uploadVideo } from '../tests/utils/index' 5import { uploadVideo } from '../../shared/utils/'
6import { VideoPrivacy } from '../../shared/models/videos' 6import { VideoPrivacy } from '../../shared/models/videos'
7import { netrc, getSettings } from './cli' 7import { netrc, getSettings } from './cli'
8 8